diff --git a/lib/irc/nola_irc.ex b/lib/irc/nola_irc.ex
index 2d355d3..7e23f50 100644
--- a/lib/irc/nola_irc.ex
+++ b/lib/irc/nola_irc.ex
@@ -1,33 +1,33 @@
 defmodule Nola.IRC do
   require Logger
 
   def env(), do: Nola.env(:irc)
   def env(key, default \\ nil), do: Keyword.get(env(), key, default)
 
   def application_childs do
     import Supervisor.Spec
 
     IRC.Connection.setup()
     Nola.Plugins.setup()
 
     [
       worker(Registry, [[keys: :duplicate, name: IRC.ConnectionPubSub]], id: :registr_irc_conn),
-      worker(IRC.Membership, []),
+      worker(Nola.Membership, []),
       worker(Nola.Account, []),
       worker(IRC.UserTrack.Storage, []),
       worker(Nola.Plugins.Account, []),
       supervisor(Nola.Plugins.Supervisor, [], [name: Nola.Plugins.Supervisor]),
       supervisor(IRC.Connection.Supervisor, [], [name: IRC.Connection.Supervisor]),
       supervisor(IRC.PuppetConnection.Supervisor, [], [name: IRC.PuppetConnection.Supervisor]),
     ]
   end
 
   # Start plugins first to let them get on connection events.
   def after_start() do
     Logger.info("Starting plugins")
     Nola.Plugins.start_all()
     Logger.info("Starting connections")
     IRC.Connection.start_all()
   end
 
 end
diff --git a/lib/irc/user_track.ex b/lib/irc/user_track.ex
index 3f144d5..56a319f 100644
--- a/lib/irc/user_track.ex
+++ b/lib/irc/user_track.ex
@@ -1,329 +1,329 @@
 defmodule IRC.UserTrack do
   @moduledoc """
   User Track DB & Utilities
   """
 
   @ets IRC.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 || IRC.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: IRC.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
 
       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: IRC.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
-      IRC.Membership.touch(user.account, sender.network, channel)
+      Nola.Membership.touch(user.account, sender.network, channel)
     end
 
     Storage.op(fn(ets) ->
       :ets.insert(ets, User.to_tuple(user))
     end)
 
     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(%IRC.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: IRC.Membership.touch(account, network, chan)
+    if chan, do: Nola.Membership.touch(account, network, chan)
     if !m.account do
       {:ok, %IRC.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
       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))
       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
-        IRC.Membership.touch(user.account, network, channel)
+        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))
         IRC.Connection.publish_event({network, channel}, %{type: :part, user_id: user.id, account_id: user.account, reason: nil})
       else
         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
-          IRC.Membership.touch(user.account, sender.network, channel)
+          Nola.Membership.touch(user.account, sender.network, channel)
         end
         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
diff --git a/lib/matrix/room.ex b/lib/matrix/room.ex
index 4c5cf7e..57f35b8 100644
--- a/lib/matrix/room.ex
+++ b/lib/matrix/room.ex
@@ -1,196 +1,196 @@
 defmodule Nola.Matrix.Room do
   require Logger
   alias Nola.Matrix
   alias Polyjuice.Client
   import Matrix, only: [client: 0, client: 1, user_name: 1, myself?: 1]
 
   defmodule Supervisor do
     use DynamicSupervisor
 
     def start_link() do
       DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
     end
 
     def start_child(room_id) do
       spec = %{id: room_id, start: {Nola.Matrix.Room, :start_link, [room_id]}, restart: :transient}
       DynamicSupervisor.start_child(__MODULE__, spec)
     end
 
     @impl true
     def init(_init_arg) do
       DynamicSupervisor.init(
         strategy: :one_for_one,
         max_restarts: 10,
         max_seconds: 1
       )
     end
   end
 
   def start(room_id) do
     __MODULE__.Supervisor.start_child(room_id)
   end
 
   def start_link(room_id) do
     GenServer.start_link(__MODULE__, [room_id], name: name(room_id))
   end
 
   def start_and_send_matrix_event(room_id, event) do
     pid = if pid = whereis(room_id) do
       pid
     else
       case __MODULE__.start(room_id) do
         {:ok, pid} -> pid
         {:error, {:already_started, pid}} -> pid
         :ignore -> nil
       end
     end
     if(pid, do: send(pid, {:matrix_event, event}))
   end
 
   def whereis(room_id) do
     {:global, name} = name(room_id)
     case :global.whereis_name(name) do
       :undefined -> nil
       pid -> pid
     end
   end
 
   def name(room_id) do
     {:global, {__MODULE__, room_id}}
   end
 
   def init([room_id]) do
     case Matrix.lookup_room(room_id) do
       {:ok, state} ->
         Logger.metadata(matrix_room: room_id)
 
         {:ok, _} = Registry.register(Nola.PubSub, "#{state.network}:events", plugin: __MODULE__)
         for t <- ["messages", "triggers", "outputs", "events"] do
           {:ok, _} = Registry.register(Nola.PubSub, "#{state.network}/#{state.channel}:#{t}", plugin: __MODULE__)
         end
 
         state = state
         |> Map.put(:id, room_id)
         Logger.info("Started Matrix room #{room_id}")
         {:ok, state, {:continue, :update_state}}
       error ->
         Logger.info("Received event for nonexistent room #{inspect room_id}: #{inspect error}")
         :ignore
     end
   end
 
   def handle_continue(:update_state, state) do
     {:ok, s} = Client.Room.get_state(client(), state.id)
     members = Enum.reduce(s, [], fn(s, acc) ->
       if s["type"] == "m.room.member" do
         if s["content"]["membership"] == "join" do
           [s["user_id"] | acc]
         else
-          # XXX: The user left, remove from IRC.Memberships ?
+          # XXX: The user left, remove from Nola.Memberships ?
           acc
         end
       else
         acc
       end
     end)
     |> Enum.filter(& &1)
 
     for m <- members, do: IRC.UserTrack.joined(state.id, %{network: "matrix", nick: m, user: m, host: "matrix."}, [], true)
 
     accounts = IRC.UserTrack.channel(state.network, state.channel)
     |> Enum.filter(& &1)
     |> Enum.map(fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple).account end)
     |> Enum.uniq()
     |> Enum.each(fn(account_id) ->
       introduce_irc_account(account_id, state)
     end)
 
     {:noreply, state}
   end
 
   def handle_info({:irc, :text, message}, state), do: handle_irc(message, state)
   def handle_info({:irc, :out, message}, state), do: handle_irc(message, state)
   def handle_info({:irc, :trigger, _, message}, state), do: handle_irc(message, state)
   def handle_info({:irc, :event, event}, state), do: handle_irc(event, state)
   def handle_info({:matrix_event, event}, state) do
     if myself?(event.user_id) do
       {:noreply, state}
     else
       handle_matrix(event, state)
     end
   end
 
   def handle_irc(message = %IRC.Message{account: account}, state) do
     unless Map.get(message.meta, :puppet) && Map.get(message.meta, :from) == self() do
         opts = if Map.get(message.meta, :self) || is_nil(account) do
         []
       else
         mxid = Matrix.get_or_create_matrix_user(account.id)
         [user_id: mxid]
       end
       Client.Room.send_message(client(opts),state.id, message.text)
     end
     {:noreply, state}
   end
 
   def handle_irc(%{type: :join, account_id: account_id}, state) do
     introduce_irc_account(account_id, state)
     {:noreply, state}
   end
 
   def handle_irc(%{type: quit_or_part, account_id: account_id}, state) when quit_or_part in [:quit, :part] do
     mxid = Matrix.get_or_create_matrix_user(account_id)
     Client.Room.leave(client(user_id: mxid), state.id)
     {:noreply, state}
   end
 
 
   def handle_irc(event, state) do
     Logger.warn("Skipped irc event #{inspect event}")
     {:noreply, state}
   end
 
   def handle_matrix(event = %{type: "m.room.member", user_id: user_id, content: %{"membership" => "join"}}, state) do
     _account = get_account(event, state)
     IRC.UserTrack.joined(state.id, %{network: "matrix", nick: user_id, user: user_id, host: "matrix."}, [], true)
     {:noreply, state}
   end
 
   def handle_matrix(event = %{type: "m.room.member", user_id: user_id, content: %{"membership" => "leave"}}, state) do
     IRC.UserTrack.parted(state.id, %{network: "matrix", nick: user_id})
     {:noreply, state}
   end
 
   def handle_matrix(event = %{type: "m.room.message", user_id: user_id, content: %{"msgtype" => "m.text", "body" => text}}, state) do
     IRC.send_message_as(get_account(event, state), state.network, state.channel, text, true)
     {:noreply, state}
   end
 
   def handle_matrix(event, state) do
     Logger.warn("Skipped matrix event #{inspect event}")
     {:noreply, state}
   end
 
   def get_account(%{user_id: user_id}, %{id: id}) do
     Nola.Account.find_by_nick("matrix", user_id)
   end
 
   defp introduce_irc_account(account_id, state) do
     mxid = Matrix.get_or_create_matrix_user(account_id)
     account = Nola.Account.get(account_id)
     user = IRC.UserTrack.find_by_account(state.network, account)
     base_nick = if(user, do: user.nick, else: account.name)
     case Client.Profile.put_displayname(client(user_id: mxid), base_nick) do
       :ok -> :ok
       error ->
         Logger.warn("Failed to update profile for #{mxid}: #{inspect error}")
     end
     case Client.Room.join(client(user_id: mxid), state.id) do
       {:ok, _} -> :ok
       error ->
         Logger.warn("Failed to join room for #{mxid}: #{inspect error}")
     end
     :ok
   end
 
 end
diff --git a/lib/nola/account.ex b/lib/nola/account.ex
index cd424ef..4f1e7ff 100644
--- a/lib/nola/account.ex
+++ b/lib/nola/account.ex
@@ -1,263 +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)
+      Nola.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)
+        memberships = Nola.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
diff --git a/lib/irc/membership.ex b/lib/nola/membership.ex
similarity index 99%
rename from lib/irc/membership.ex
rename to lib/nola/membership.ex
index 25a0cfc..b98efd7 100644
--- a/lib/irc/membership.ex
+++ b/lib/nola/membership.ex
@@ -1,129 +1,129 @@
-defmodule IRC.Membership do
+defmodule Nola.Membership do
   @moduledoc """
   Memberships (users in channels)
   """
 
   # Key: {account, net, channel}
   # Format: {key, last_seen}
 
   defp dets() do
     to_charlist(Nola.data_path <> "/memberships.dets")
   end
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], [name: __MODULE__])
   end
 
   def init(_) do
     dets = :dets.open_file(dets(), [])
     {:ok, dets}
   end
 
   def of_account(%Nola.Account{id: id}) do
     spec = [{{{:"$1", :"$2", :"$3"}, :_}, [{:==, :"$1", {:const, id}}], [{{:"$2", :"$3"}}]}]
     :dets.select(dets(), spec)
   end
 
   def merge_account(old_id, new_id) do
     #iex(37)> :ets.fun2ms(fn({{old_id, _, _}, _}=obj) when old_id == "42" -> obj end)
     spec = [{{{:"$1", :_, :_}, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
     Util.ets_mutate_select_each(:dets, dets(), spec, fn(table, obj = {{_old, net, chan}, ts}) ->
       :dets.delete_object(table, obj)
       :dets.insert(table, {{new_id, net, chan}, ts})
     end)
   end
 
   def touch(%Nola.Account{id: id}, network, channel) do
     :dets.insert(dets(), {{id, network, channel}, NaiveDateTime.utc_now()})
   end
   def touch(account_id, network, channel) do
     if account = Nola.Account.get(account_id) do
       touch(account, network, channel)
     end
   end
 
   def notify_channels(account, minutes \\ 30, last_active \\ true) do
     not_before = NaiveDateTime.add(NaiveDateTime.utc_now(), (minutes*-60), :second)
     spec = [{{{:"$1", :_, :_}, :_}, [{:==, :"$1", {:const, account.id}}], [:"$_"]}]
     memberships = :dets.select(dets(), spec)
                   |> Enum.sort_by(fn({_, ts}) -> ts end, {:desc, NaiveDateTime})
     active_memberships = Enum.filter(memberships, fn({_, ts}) ->  NaiveDateTime.compare(ts, not_before) == :gt end)
     cond do
       active_memberships == [] && last_active ->
         case memberships do
           [{{_, net, chan}, _}|_] -> [{net, chan}]
           _ -> []
         end
       active_memberships == [] ->
         []
       true ->
         Enum.map(active_memberships, fn({{_, net, chan}, _}) -> {net,chan} end)
     end
   end
 
   def members_or_friends(account, _network, nil) do
     friends(account)
   end
 
   def members_or_friends(_, network, channel) do
     members(network, channel)
   end
 
   def expanded_members_or_friends(account, network, channel) do
     expand(network, members_or_friends(account, network, channel))
   end
 
   def expanded_members(network, channel) do
     expand(network, members(network, channel))
   end
 
   def members(network, channel) do
     #iex(19)> :ets.fun2ms(fn({{id, net, chan}, ts}) when net == network and chan == channel and ts > min_seen -> id end)
     limit = 0 # NaiveDateTime.add(NaiveDateTime.utc_now, 30*((24*-60)*60), :second)
     spec = [
       {{{:"$1", :"$2", :"$3"}, :"$4"},
        [
          {:andalso,
           {:andalso, {:==, :"$2", {:const, network}}, {:==, :"$3", {:const, channel}}},
           {:>, :"$4", {:const, limit}}}
        ], [:"$1"]}
     ]
     :dets.select(dets(), spec)
   end
 
   def friends(account = %Nola.Account{id: id}) do
     for({net, chan} <- of_account(account), do: members(net, chan))
     |> List.flatten()
     |> Enum.uniq()
   end
 
   def handle_info(_, dets) do
     {:noreply, dets}
   end
 
   def handle_cast(_, dets) do
     {:noreply, dets}
   end
 
   def handle_call(_, _, dets) do
     {:noreply, dets}
   end
 
   def terminate(_, dets) do
     :dets.sync(dets)
     :dets.close(dets)
   end
 
   defp expand(network, list) do
     for id <- list do
       if account = Nola.Account.get(id) do
         user = IRC.UserTrack.find_by_account(network, account)
         nick = if(user, do: user.nick, else: account.name)
         {account, user, nick}
       end
     end
     |> Enum.filter(fn(x) -> x end)
   end
 
 
 end
diff --git a/lib/plugins/alcoolog.ex b/lib/plugins/alcoolog.ex
index 9958889..738be71 100644
--- a/lib/plugins/alcoolog.ex
+++ b/lib/plugins/alcoolog.ex
@@ -1,1229 +1,1229 @@
 defmodule Nola.Plugins.Alcoolog do
   require Logger
 
   @moduledoc """
   # [alcoolog]({{context_path}}/alcoolog)
 
   * **!santai `<cl | (calc)>` `<degrés d'alcool> [annotation]`**: enregistre un nouveau verre de `montant` d'une boisson à `degrés d'alcool`.
   * **!santai `<cl | (calc)>` `<beer name>`**: enregistre un nouveau verre de `cl` de la bière `beer name`, et checkin sur Untappd.com.
   * **!moar `[cl]` : enregistre un verre équivalent au dernier !santai.
   * **-santai**: annule la dernière entrée d'alcoolisme.
   * **.alcoolisme**: état du channel en temps réel.
   * **.alcoolisme `<semaine | Xj>`**: points par jour, sur X j.
   * **!alcoolisme `[pseudo]`**: affiche les points d'alcoolisme.
   * **!alcoolisme `[pseudo]` `<semaine | Xj>`**: affiche les points d'alcoolisme par jour sur X j.
   * **+alcoolisme `<h|f>` `<poids en kg>` `[facteur de perte en mg/l (10, 15, 20, 25)]`**: Configure votre profil d'alcoolisme.
   * **.sobre**: affiche quand la sobriété frappera sur le chan.
   * **!sobre `[pseudo]`**: affiche quand la sobriété frappera pour `[pseudo]`.
   * **!sobrepour `<date>`**: affiche tu pourras être sobre pour `<date>`, et si oui, combien de volumes d'alcool peuvent encore être consommés.
   * **!alcoolog**: ([voir]({{context_path}}/alcoolog)) lien pour voir l'état/statistiques et historique de l'alcoolémie du channel.
   * **!alcool `<cl>` `<degrés>`**: donne le nombre d'unités d'alcool dans `<cl>` à `<degrés>°`.
   * **!soif**: c'est quand l'apéro ?
 
   1 point = 1 volume d'alcool.
 
   Annotation: champ libre!
 
   ---
 
   ## `!txt`s
 
   * status utilisateur: `alcoolog.user_(sober|legal|legalhigh|high|toohigh|sick)(|_rising)`
   * mauvaises boissons: `alcoolog.drink_(negative|zero|negative)`
   * santo: `alcoolog.santo`
   * santai: `alcoolog.santai`
   * plus gros, moins gros: `alcoolog.(fatter|thinner)`
 
   """
 
   def irc_doc, do: @moduledoc
 
   def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
 
   # tuple dets: {nick, date, volumes, current_alcohol_level, nom, commentaire}
   # tuple ets:  {{nick, date}, volumes, current, nom, commentaire}
   # tuple meta dets: {nick, map}
   #                   %{:weight => float, :sex => true(h),false(f)}
   @pubsub ~w(account)
   @pubsub_triggers ~w(santai moar again bis santo santeau alcoolog sobre sobrepour soif alcoolisme alcool)
   @default_user_meta %{weight: 77.4, sex: true, loss_factor: 15}
 
   def data_state() do
     dets_filename = (Nola.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
     dets_meta_filename = (Nola.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist
     %{dets: dets_filename, meta: dets_meta_filename, ets: __MODULE__.ETS}
   end
 
   def init(_) do
     triggers = for(t <- @pubsub_triggers, do: "trigger:"<>t)
     for sub <- @pubsub ++ triggers do
       {:ok, _} = Registry.register(Nola.PubSub, sub, plugin: __MODULE__)
     end
     dets_filename = (Nola.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
     {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
     ets = :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
     dets_meta_filename = (Nola.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist
     {:ok, meta} = :dets.open_file(dets_meta_filename, [{:type,:set}])
     traverse_fun = fn(obj, dets) ->
       case obj do
         object = {nick, naive = %NaiveDateTime{}, volumes, active, cl, deg, name, comment} ->
           date = naive
           |> DateTime.from_naive!("Etc/UTC")
           |> DateTime.to_unix()
           new = {nick, date, volumes, active, cl, deg, name, comment, Map.new()}
           :dets.delete_object(dets, object)
           :dets.insert(dets, new)
           :ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, Map.new()})
           dets
 
         object = {nick, naive = %NaiveDateTime{}, volumes, active, cl, deg, name, comment, meta} ->
           date = naive
           |> DateTime.from_naive!("Etc/UTC")
           |> DateTime.to_unix()
           new = {nick, date, volumes, active, cl, deg, name, comment, Map.new()}
           :dets.delete_object(dets, object)
           :dets.insert(dets, new)
           :ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, Map.new()})
           dets
 
         object = {nick, date, volumes, active, cl, deg, name, comment, meta} ->
           :ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, meta})
           dets
 
         _ ->
           dets
       end
     end
     :dets.foldl(traverse_fun, dets, dets)
     :dets.sync(dets)
     state = %{dets: dets, meta: meta, ets: ets}
     {:ok, state}
   end
 
   @eau ["santo", "santeau"]
   def handle_info({:irc, :trigger, santeau, m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) when santeau in @eau do
     Nola.Plugins.Txt.reply_random(m, "alcoolog.santo")
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "soif", m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) do
     now = DateTime.utc_now()
           |> Timex.Timezone.convert("Europe/Paris")
     apero = format_duration_from_now(%DateTime{now | hour: 18, minute: 0, second: 0}, false)
     day_of_week = Date.day_of_week(now)
     {txt, apero?} = cond do
       now.hour >= 0 && now.hour < 6 ->
         {["apéro tardif ? Je dis OUI ! SANTAI !"], true}
       now.hour >= 6 && now.hour < 12 ->
         if day_of_week >= 6 do
           {["de l'alcool pour le petit dej ? le week-end, pas de problème !"], true}
         else
           {["C'est quand même un peu tôt non ? Prochain apéro #{apero}"], false}
         end
       now.hour >= 12 && (now.hour < 14) ->
         {["oui! c'est l'apéro de midi! (et apéro #{apero})",
           "tu peux attendre #{apero} ou y aller, il est midi !"
         ], true}
       now.hour == 17 ->
         {[
           "ÇA APPROCHE !!! Apéro #{apero}",
           "BIENTÔT !!! Apéro #{apero}",
           "achetez vite les teilles, apéro dans #{apero}!",
           "préparez les teilles, apéro dans #{apero}!"
         ], false}
       now.hour >= 14 && now.hour < 18 ->
         weekend = if day_of_week >= 6 do
           " ... ou maintenant en fait, c'est le week-end!"
         else
           ""
         end
         {["tiens bon! apéro #{apero}#{weekend}",
           "courage... apéro dans #{apero}#{weekend}",
           "pas encore :'( apéro dans #{apero}#{weekend}"
         ], false}
       true ->
       {[
           "C'EST L'HEURE DE L'APÉRO !!! SANTAIIIIIIIIIIII !!!!"
         ], true}
     end
 
     txt = txt
           |> Enum.shuffle()
           |> Enum.random()
 
     m.replyfun.(txt)
 
     stats = get_full_statistics(state, m.account.id)
     if !apero? && stats.active > 0.1 do
       m.replyfun.("(... ou continue en fait, je suis pas ta mère !)")
     end
 
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "sobrepour", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
     args = Enum.join(args, " ")
     {:ok, now} = DateTime.now("Europe/Paris", Tzdata.TimeZoneDatabase)
     time = case args do
       "demain " <> time ->
         {h, m} = case String.split(time, [":", "h"]) do
           [hour, ""] ->
             IO.puts ("h #{inspect hour}")
             {h, _} = Integer.parse(hour)
             {h, 0}
           [hour, min] when min != "" ->
             {h, _} = Integer.parse(hour)
             {m, _} = Integer.parse(min)
             {h, m}
           [hour] ->
             IO.puts ("h #{inspect hour}")
             {h, _} = Integer.parse(hour)
             {h, 0}
           _ -> {0, 0}
         end
         secs = ((60*60)*24)
         day = DateTime.add(now, secs, :second, Tzdata.TimeZoneDatabase)
         %DateTime{day | hour: h, minute: m, second: 0}
       "après demain " <> time ->
         secs = 2*((60*60)*24)
         DateTime.add(now, secs, :second, Tzdata.TimeZoneDatabase)
       datetime ->
         case Timex.Parse.DateTime.Parser.parse(datetime, "{}") do
           {:ok, dt} -> dt
           _ -> nil
         end
     end
 
     if time do
       meta = get_user_meta(state, m.account.id)
       stats = get_full_statistics(state, m.account.id)
 
       duration = round(DateTime.diff(time, now)/60.0)
 
       IO.puts "diff #{inspect duration} sober in #{inspect stats.sober_in}"
 
       if duration < stats.sober_in do
         int = stats.sober_in - duration
         m.replyfun.("désolé, aucune chance! tu seras sobre #{format_minute_duration(int)} après!")
       else
         remaining = duration - stats.sober_in
         if remaining < 30 do
           m.replyfun.("moins de 30 minutes de sobriété, c'est impossible de boire plus")
         else
           loss_per_minute = ((meta.loss_factor/100)/60)
           remaining_gl = (remaining-30)*loss_per_minute
           m.replyfun.("marge de boisson: #{inspect remaining} minutes, #{remaining_gl} g/l")
         end
       end
 
     end
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "alcoolog", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :plus}}}, state) do
     {:ok, token} = Nola.Token.new({:alcoolog, :index, m.sender.network, m.channel})
     url = NolaWeb.Router.Helpers.alcoolog_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel), token)
     m.replyfun.("-> #{url}")
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "alcoolog", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :bang}}}, state) do
     url = NolaWeb.Router.Helpers.alcoolog_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel))
     m.replyfun.("-> #{url}")
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "alcool", m = %IRC.Message{trigger: %IRC.Trigger{args: args = [cl, deg], type: :bang}}}, state) do
     {cl, _} = Util.float_paparse(cl)
     {deg, _} = Util.float_paparse(deg)
     points = Alcool.units(cl, deg)
     meta = get_user_meta(state, m.account.id)
     k = if meta.sex, do: 0.7, else: 0.6
     weight = meta.weight
     gl = (10*points)/(k*weight)
     duration = round(gl/((meta.loss_factor/100)/60))+30
         sober_in_s = if duration > 0 do
         duration = Timex.Duration.from_minutes(duration)
           Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
         else
           ""
         end
 
     m.replyfun.("Il y a #{Float.round(points+0.0, 4)} unités d'alcool dans #{cl}cl à #{deg}° (#{Float.round(gl + 0.0, 4)} g/l, #{sober_in_s})")
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: [cl, deg | comment], type: :bang}}}, state) do
     santai(m, state, cl, deg, comment)
     {:noreply, state}
   end
 
   @moar [
     "{{message.sender.nick}}: la même donc ?",
     "{{message.sender.nick}}: et voilà la petite sœur !"
   ]
 
   def handle_info({:irc, :trigger, "bis", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
     handle_info({:irc, :trigger, "moar", m}, state)
   end
   def handle_info({:irc, :trigger, "again", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
     handle_info({:irc, :trigger, "moar", m}, state)
   end
 
   def handle_info({:irc, :trigger, "moar", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
     case get_statistics_for_nick(state, m.account.id) do
       {_, obj = {_, _date, _points, _active, cl, deg, _name, comment, _meta}} ->
         cl = case args do
           [cls] ->
             case Util.float_paparse(cls) do
               {cl, _} -> cl
               _ -> cl
             end
           _ -> cl
         end
         moar = @moar |> Enum.shuffle() |> Enum.random() |> Tmpl.render(m) |> m.replyfun.()
         santai(m, state, cl, deg, comment, auto_set: true)
       {_, obj = {_, date, points, _last_active, type, descr}} ->
         case Regex.named_captures(~r/^(?<cl>\d+[.]\d+)cl\s+(?<deg>\d+[.]\d+)°$/, type) do
           nil -> m.replyfun.("suce")
           u ->
             moar = @moar |> Enum.shuffle() |> Enum.random() |> Tmpl.render(m) |> m.replyfun.()
             santai(m, state, u["cl"], u["deg"], descr, auto_set: true)
         end
       _ -> nil
     end
     {:noreply, state}
   end
 
   defp santai(m, state, cl, deg, comment, options \\ []) do
     comment = cond do
       comment == [] -> nil
       is_binary(comment) -> comment
       comment == nil -> nil
       true -> Enum.join(comment, " ")
     end
 
     {cl, cl_extra} = case {Util.float_paparse(cl), cl} do
       {{cl, extra}, _} -> {cl, extra}
     {:error, "("<>_} ->
         try do
           {:ok, result} = Abacus.eval(cl)
           {result, nil}
         rescue
           _ -> {nil, "cl: invalid calc expression"}
         end
       {:error, _} -> {nil, "cl: invalid value"}
     end
 
     {deg, comment, auto_set, beer_id} = case Util.float_paparse(deg) do
       {deg, _} -> {deg, comment, Keyword.get(options, :auto_set, false), nil}
       :error ->
         beername = if(comment, do: "#{deg} #{comment}", else: deg)
         case Untappd.search_beer(beername, limit: 1) do
           {:ok, %{"response" => %{"beers" => %{"count" => count, "items" => [%{"beer" => beer, "brewery" => brewery} | _]}}}} ->
             {Map.get(beer, "beer_abv"), "#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")}", true, Map.get(beer, "bid")}
           _ ->
               {deg, "could not find beer", false, nil}
         end
     end
 
     cond do
       cl == nil -> m.replyfun.(cl_extra)
       deg == nil -> m.replyfun.(comment)
       cl >= 500 || deg >= 100 -> Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_toohuge")
       cl == 0 || deg == 0 -> Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_zero")
       cl < 0 || deg < 0 -> Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_negative")
       true ->
         points = Alcool.units(cl, deg)
         now = m.at || DateTime.utc_now()
               |> DateTime.to_unix(:millisecond)
         user_meta = get_user_meta(state, m.account.id)
         name = "#{cl}cl #{deg}°"
         old_stats = get_full_statistics(state, m.account.id)
         meta = %{}
         meta = Map.put(meta, "timestamp", now)
         meta = Map.put(meta, "weight", user_meta.weight)
         meta = Map.put(meta, "sex", user_meta.sex)
         :ok = :dets.insert(state.dets, {m.account.id, now, points, if(old_stats, do: old_stats.active, else: 0), cl, deg, name, comment, meta})
         true = :ets.insert(state.ets, {{m.account.id, now}, points, if(old_stats, do: old_stats.active, else: 0),cl, deg, name, comment, meta})
         #sante = @santai |> Enum.map(fn(s) -> String.trim(String.upcase(s)) end) |> Enum.shuffle() |> Enum.random()
         sante = Nola.Plugins.Txt.random("alcoolog.santai")
         k = if user_meta.sex, do: 0.7, else: 0.6
         weight = user_meta.weight
         peak = Float.round((10*points||0.0)/(k*weight), 4)
         stats = get_full_statistics(state, m.account.id)
         sober_add = if old_stats && Map.get(old_stats || %{}, :sober_in) do
           mins = round(stats.sober_in - old_stats.sober_in)
           " [+#{mins}m]"
         else
           ""
         end
         nonow = DateTime.utc_now()
         sober = nonow |> DateTime.add(round(stats.sober_in*60), :second)
               |> Timex.Timezone.convert("Europe/Paris")
         at = if nonow.day == sober.day do
           {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
           detail
         else
           {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
           detail
         end
 
         up = if stats.active_drinks > 1 do
           " " <> Enum.join(for(_ <- 1..stats.active_drinks, do: "▲")) <> ""
         else
           ""
         end
 
         since_str = if stats.since && stats.since_min > 180 do
         "(depuis: #{stats.since_s}) "
       else
         ""
       end
 
         msg = fn(nick, extra) ->
           "#{sante} #{nick} #{extra}#{up} #{format_points(points)} @#{stats.active}g/l [+#{peak} g/l]"
           <> " (15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) #{since_str}(sobriété #{at} (dans #{stats.sober_in_s})#{sober_add}) !"
           <> " (aujourd'hui #{stats.daily_volumes} points - #{stats.daily_gl} g/l)"
         end
 
         meta = if beer_id do
           Map.put(meta, "untappd:beer_id", beer_id)
         else
           meta
         end
 
         if beer_id do
           spawn(fn() ->
             case Untappd.maybe_checkin(m.account, beer_id) do
               {:ok, body} ->
                 badges = get_in(body, ["badges", "items"])
                 if badges != [] do
                   badges_s = Enum.map(badges, fn(badge) -> Map.get(badge, "badge_name") end)
                            |> Enum.filter(fn(b) -> b end)
                            |> Enum.intersperse(", ")
                            |> Enum.join("")
                     badge = if(length(badges) > 1, do: "badges", else: "badge")
                     m.replyfun.("\\O/ Unlocked untappd #{badge}: #{badges_s}")
                 end
                 :ok
               {:error, {:http_error, error}} when is_integer(error) -> m.replyfun.("Checkin to Untappd failed: #{to_string(error)}")
               {:error, {:http_error, error}} -> m.replyfun.("Checkin to Untappd failed: #{inspect error}")
               _ -> :error
             end
           end)
         end
 
         local_extra = if auto_set do
           if comment do
             " #{comment} (#{cl}cl @ #{deg}°)"
           else
             "#{cl}cl @ #{deg}°"
           end
         else
           ""
         end
         m.replyfun.(msg.(m.sender.nick, local_extra))
-        notify = IRC.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
+        notify = Nola.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
         for {net, chan} <- notify do
           user = IRC.UserTrack.find_by_account(net, m.account)
           nick = if(user, do: user.nick, else: m.account.name)
           extra = " " <> present_type(name, comment) <> ""
           IRC.Connection.broadcast_message(net, chan, msg.(nick, extra))
         end
 
         miss = cond do
           points <= 0.6 -> :small
           stats.active30m >= 2.9 && stats.active30m < 3 -> :miss3
           stats.active30m >= 1.9 && stats.active30m < 2 -> :miss2
           stats.active30m >= 0.9 && stats.active30m < 1 -> :miss1
           stats.active30m >= 0.45 && stats.active30m < 0.5 -> :miss05
           stats.active30m >= 0.20 && stats.active30m < 0.20 -> :miss025
           stats.active30m >= 3 && stats.active1h < 3.15 -> :small3
           stats.active30m >= 2 && stats.active1h < 2.15 -> :small2
           stats.active30m >= 1.5 && stats.active1h < 1.5 -> :small15
           stats.active30m >= 1 && stats.active1h < 1.15 -> :small1
           stats.active30m >= 0.5 && stats.active1h <= 0.51 -> :small05
           stats.active30m >= 0.25 && stats.active30m <= 0.255 -> :small025
           true -> nil
         end
 
         if miss do
           miss = Nola.Plugins.Txt.random("alcoolog.#{to_string(miss)}")
           if miss do
-            for {net, chan} <- IRC.Membership.notify_channels(m.account) do
+            for {net, chan} <- Nola.Membership.notify_channels(m.account) do
               user = IRC.UserTrack.find_by_account(net, m.account)
               nick = if(user, do: user.nick, else: m.account.name)
               IRC.Connection.broadcast_message(net, chan, "#{nick}: #{miss}")
             end
           end
         end
     end
   end
 
   def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) do
     m.replyfun.("!santai <cl> <degrés> [commentaire]")
     {:noreply, state}
   end
 
   def get_all_stats() do
     Nola.Account.all_accounts()
     |> Enum.map(fn(account) -> {account.id, get_full_statistics(account.id)} end)
     |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
     |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
   end
 
   def get_channel_statistics(account, network, nil) do
-    IRC.Membership.expanded_members_or_friends(account, network, nil)
+    Nola.Membership.expanded_members_or_friends(account, network, nil)
     |> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(account.id)} end)
     |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
     |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
   end
 
   def get_channel_statistics(_, network, channel), do: get_channel_statistics(network, channel)
 
   def get_channel_statistics(network, channel) do
-    IRC.Membership.expanded_members(network, channel)
+    Nola.Membership.expanded_members(network, channel)
     |> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(account.id)} end)
     |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
     |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
   end
 
   @spec since() :: %{Nola.Account.id() => DateTime.t()}
   @doc "Returns the last time the user was at 0 g/l"
   def since() do
     :ets.foldr(fn({{acct, timestamp_or_date}, _vol, current, _cl, _deg, _name, _comment, _m}, acc) ->
       if !Map.get(acc, acct) && current == 0 do
         date = Util.to_date_time(timestamp_or_date)
         Map.put(acc, acct, date)
       else
         acc
       end
     end, %{}, __MODULE__.ETS)
   end
 
   def get_full_statistics(nick) do
     get_full_statistics(data_state(), nick)
   end
 
   defp get_full_statistics(state, nick) do
     case get_statistics_for_nick(state, nick) do
       {count, {_, last_at, last_points, last_active, last_cl, last_deg, last_type, last_descr, _meta}} ->
         {active, active_drinks} = current_alcohol_level(state, nick)
         {_, m30} = alcohol_level_rising(state, nick)
         {rising, m15} = alcohol_level_rising(state, nick, 15)
         {_, m5} = alcohol_level_rising(state, nick, 5)
         {_, h1} = alcohol_level_rising(state, nick, 60)
 
         trend = if rising do
           "▲"
         else
           "▼"
         end
       user_state = cond do
         active <= 0.0 -> :sober
         active <= 0.25 -> :low
         active <= 0.50 -> :legal
         active <= 1.0 -> :legalhigh
         active <= 2.5 -> :high
         active < 3 -> :toohigh
         true -> :sick
         end
 
       rising_file_key = if rising, do: "_rising", else: ""
       txt_file = "alcoolog." <> "user_" <> to_string(user_state) <> rising_file_key
       user_status = Nola.Plugins.Txt.random(txt_file)
 
       meta = get_user_meta(state, nick)
         minutes_til_sober = h1/((meta.loss_factor/100)/60)
         minutes_til_sober = cond do
           active < 0 -> 0
           m15 < 0 -> 15
           m30 < 0 -> 30
           h1 < 0 -> 60
           minutes_til_sober > 0 ->
             Float.round(minutes_til_sober+60)
           true -> 0
         end
 
         duration = Timex.Duration.from_minutes(minutes_til_sober)
         sober_in_s = if minutes_til_sober > 0 do
           Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
         else
           nil
         end
 
         since = if active > 0 do
               since()
               |> Map.get(nick)
         end
 
         since_diff = if since, do: Timex.diff(DateTime.utc_now(), since, :minutes)
         since_duration = if since, do: Timex.Duration.from_minutes(since_diff)
         since_s = if since, do: Timex.Format.Duration.Formatter.lformat(since_duration, "fr", :humanized)
 
       {total_volumes, total_gl} = user_stats(state, nick)
 
 
         %{active: active, last_at: last_at, last_cl: last_cl, last_deg: last_deg, last_points: last_points, last_type: last_type, last_descr: last_descr,
           trend_symbol: trend,
           active5m: m5, active15m: m15, active30m: m30, active1h: h1,
           rising: rising,
           active_drinks: active_drinks,
           user_status: user_status,
           daily_gl: total_gl, daily_volumes: total_volumes,
           sober_in: minutes_til_sober, sober_in_s: sober_in_s,
           since: since, since_min: since_diff, since_s: since_s,
         }
     _ ->
       nil
     end
   end
 
   def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :dot}}}, state) do
-    nicks = IRC.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
+    nicks = Nola.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
     |> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(state, account.id)} end)
     |> Enum.filter(fn({_nick, status}) -> status && status.sober_in && status.sober_in > 0 end)
     |> Enum.sort_by(fn({_, status}) -> status.sober_in end, &</2)
     |> Enum.map(fn({nick, stats}) ->
       now = DateTime.utc_now()
       sober = now |> DateTime.add(round(stats.sober_in*60), :second)
             |> Timex.Timezone.convert("Europe/Paris")
         at = if now.day == sober.day do
           {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
           detail
         else
           {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
           detail
         end
       "#{nick} sobre #{at} (dans #{stats.sober_in_s})"
     end)
     |> Enum.intersperse(", ")
     |> Enum.join("")
     |> (fn(line) ->
       case line do
         "" -> "tout le monde est sobre......."
         line -> line
       end
     end).()
     |> m.replyfun.()
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
     account = case args do
       [nick] -> Nola.Account.find_always_by_nick(m.network, m.channel, nick)
       [] -> m.account
     end
 
     if account do
       user = IRC.UserTrack.find_by_account(m.network, account)
       nick = if(user, do: user.nick, else: account.name)
       stats = get_full_statistics(state, account.id)
       if stats && stats.sober_in > 0 do
         now = DateTime.utc_now()
         sober = now |> DateTime.add(round(stats.sober_in*60), :second)
               |> Timex.Timezone.convert("Europe/Paris")
           at = if now.day == sober.day do
             {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
             detail
           else
             {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
             detail
           end
         m.replyfun.("#{nick} sera sobre #{at} (dans #{stats.sober_in_s})!")
       else
         m.replyfun.("#{nick} est déjà sobre. aidez le !")
       end
     else
       m.replyfun.("inconnu")
     end
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :dot}}}, state) do
-    nicks = IRC.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
+    nicks = Nola.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
     |> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(state, account.id)} end)
     |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
     |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
     |> Enum.map(fn({nick, status}) ->
         trend_symbol = if status.active_drinks > 1 do
           Enum.join(for(_ <- 1..status.active_drinks, do: status.trend_symbol))
         else
           status.trend_symbol
         end
       since_str = if status.since_min > 180 do
         "depuis: #{status.since_s} | "
       else
         ""
       end
       "#{nick} #{status.user_status} #{trend_symbol} #{Float.round(status.active, 4)} g/l [#{since_str}sobre dans: #{status.sober_in_s}]"
     end)
     |> Enum.intersperse(", ")
     |> Enum.join("")
 
     msg = if nicks == "" do
       "wtf?!?! personne n'a bu!"
     else
       nicks
     end
 
     m.replyfun.(msg)
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [time], type: :dot}}}, state) do
     time = case time do
       "semaine" -> 7
       string ->
         case Integer.parse(string) do
           {time, "j"} -> time
           {time, "J"} -> time
           _ -> nil
         end
     end
 
     if time do
       aday = time*((24 * 60)*60)
       now = DateTime.utc_now()
       before = now
                |> DateTime.add(-aday, :second)
                |> DateTime.to_unix(:millisecond)
       over_time_stats(before, time, m, state)
     else
       m.replyfun.(".alcooolisme semaine|Xj")
     end
     {:noreply, state}
   end
 
   def user_over_time(account, count) do
     user_over_time(data_state(), account, count)
   end
 
   def user_over_time(state, account, count) do
     delay = count*((24 * 60)*60)
     now = DateTime.utc_now()
     before = DateTime.utc_now()
              |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
              |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
              |> DateTime.to_unix(:millisecond)
     #[
 #  {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
 #   [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]}
     #]
     match = [{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
        [{:andalso, {:>, :"$2", {:const, before}}, {:==, :"$1", {:const, account.id}}}], [:"$_"]}
     ]
     :ets.select(state.ets, match)
     |> Enum.reduce(Map.new, fn({{_, ts}, vol, _, _, _, _, _, _}, acc) ->
       date = DateTime.from_unix!(ts, :millisecond)
              |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
 
       date = if date.hour <= 8 do
         DateTime.add(date, -(60*(60*(date.hour+1))), :second, Tzdata.TimeZoneDatabase)
       else
         date
       end
       |> DateTime.to_date()
 
       Map.put(acc, date, Map.get(acc, date, 0) + vol)
     end)
   end
 
   def user_over_time_gl(account, count) do
     state = data_state()
     meta = get_user_meta(state, account.id)
     delay = count*((24 * 60)*60)
     now = DateTime.utc_now()
     before = DateTime.utc_now()
              |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
              |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
              |> DateTime.to_unix(:millisecond)
     #[
 #  {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
 #   [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]}
     #]
     match = [{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
        [{:andalso, {:>, :"$2", {:const, before}}, {:==, :"$1", {:const, account.id}}}], [:"$_"]}
     ]
     :ets.select(state.ets, match)
     |> Enum.reduce(Map.new, fn({{_, ts}, vol, _, _, _, _, _, _}, acc) ->
       date = DateTime.from_unix!(ts, :millisecond)
              |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
 
       date = if date.hour <= 8 do
         DateTime.add(date, -(60*(60*(date.hour+1))), :second, Tzdata.TimeZoneDatabase)
       else
         date
       end
       |> DateTime.to_date()
       weight = meta.weight
       k = if meta.sex, do: 0.7, else: 0.6
       gl = (10*vol)/(k*weight)
 
       Map.put(acc, date, Map.get(acc, date, 0) + gl)
     end)
   end
 
 
 
   defp over_time_stats(before, j, m, state) do
     #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _, _, _, _}) when date > before -> obj end)
     match = [{{{:_, :"$1"}, :_, :_, :_, :_, :_, :_, :_},
        [{:>, :"$1", {:const, before}}], [:"$_"]}
     ]
   # tuple ets:  {{nick, date}, volumes, current, nom, commentaire}
-    members = IRC.Membership.members_or_friends(m.account, m.network, m.channel)
+    members = Nola.Membership.members_or_friends(m.account, m.network, m.channel)
     drinks = :ets.select(state.ets, match)
     |> Enum.filter(fn({{account, _}, _, _, _, _, _, _, _}) -> Enum.member?(members, account) end)
              |> Enum.sort_by(fn({{_, ts}, _, _, _, _, _, _, _}) -> ts end, &>/2)
 
     top = Enum.reduce(drinks, %{}, fn({{nick, _}, vol, _, _, _, _, _, _}, acc) ->
       all = Map.get(acc, nick, 0)
       Map.put(acc, nick, all + vol)
     end)
     |> Enum.sort_by(fn({_nick, count}) -> count end, &>/2)
     |> Enum.map(fn({nick, count}) ->
       account = Nola.Account.get(nick)
       user = IRC.UserTrack.find_by_account(m.network, account)
       nick = if(user, do: user.nick, else: account.name)
       "#{nick}: #{Float.round(count, 4)}"
     end)
     |> Enum.intersperse(", ")
 
     m.replyfun.("sur #{j} jours: #{top}")
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :plus}}}, state) do
     meta = get_user_meta(state, m.account.id)
     hf = if meta.sex, do: "h", else: "f"
     m.replyfun.("+alcoolisme sexe: #{hf} poids: #{meta.weight} facteur de perte: #{meta.loss_factor}")
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [h, weight | rest], type: :plus}}}, state) do
     h = case h do
       "h" -> true
       "f" -> false
       _ -> nil
     end
 
       weight = case Util.float_paparse(weight) do
         {weight, _} -> weight
         _ -> nil
       end
 
       {factor} = case rest do
         [factor] ->
           case Util.float_paparse(factor) do
             {float, _} -> {float}
             _ -> {@default_user_meta.loss_factor}
           end
         _ -> {@default_user_meta.loss_factor}
       end
 
       if h == nil || weight == nil do
         m.replyfun.("paramètres invalides")
       else
         old_meta = get_user_meta(state, m.account.id)
         meta = Map.merge(@default_user_meta, %{sex: h, weight: weight, loss_factor: factor})
         put_user_meta(state, m.account.id, meta)
         cond do
           old_meta.weight < meta.weight ->
             Nola.Plugins.Txt.reply_random(m, "alcoolog.fatter")
           old_meta.weight == meta.weight ->
             m.replyfun.("aucun changement!")
           true ->
             Nola.Plugins.Txt.reply_random(m, "alcoolog.thinner")
         end
       end
 
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :minus}}}, state) do
     case get_statistics_for_nick(state, m.account.id) do
       {_, obj = {_, date, points, _last_active, _cl, _deg, type, descr, _meta}} ->
         :dets.delete_object(state.dets, obj)
         :ets.delete(state.ets, {m.account.id, date})
         m.replyfun.("supprimé: #{m.sender.nick} #{points} #{type} #{descr}")
         Nola.Plugins.Txt.reply_random(m, "alcoolog.delete")
-        notify = IRC.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
+        notify = Nola.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
         for {net, chan} <- notify do
           user = IRC.UserTrack.find_by_account(net, m.account)
           nick = if(user, do: user.nick, else: m.account.name)
           IRC.Connection.broadcast_message(net, chan, "#{nick} -santai #{points} #{type} #{descr}")
         end
         {:noreply, state}
       _ ->
         {:noreply, state}
     end
   end
 
 
   def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
     {account, duration} = case args do
       [nick | rest] -> {Nola.Account.find_always_by_nick(m.network, m.channel, nick), rest}
       [] -> {m.account, []}
     end
     if account do
       duration = case duration do
         ["semaine"] -> 7
         [j] ->
           case Integer.parse(j) do
             {j, "j"} -> j
             _ -> nil
           end
         _ -> nil
       end
       user = IRC.UserTrack.find_by_account(m.network, account)
       nick = if(user, do: user.nick, else: account.name)
       if duration do
         if duration > 90 do
           m.replyfun.("trop gros, ça rentrera pas")
         else
           # duration stats
           stats = user_over_time(state, account, duration)
           |> Enum.sort_by(fn({k,_v}) -> k end, {:asc, Date})
           |> Enum.map(fn({date, count}) ->
             "#{date.day}: #{Float.round(count, 2)}"
           end)
           |> Enum.intersperse(", ")
           |> Enum.join("")
 
           if stats == "" do
             m.replyfun.("alcoolisme a zéro sur #{duration}j :/")
           else
             m.replyfun.("alcoolisme de #{nick}, #{duration} derniers jours: #{stats}")
           end
         end
       else
         if stats = get_full_statistics(state, account.id) do
           trend_symbol = if stats.active_drinks > 1 do
             Enum.join(for(_ <- 1..stats.active_drinks, do: stats.trend_symbol))
           else
             stats.trend_symbol
           end
         # TODO: Lookup nick for account_id
           msg = "#{nick} #{stats.user_status} "
           <> (if stats.active > 0 || stats.active15m > 0 || stats.active30m > 0 || stats.active1h > 0, do: ": #{trend_symbol} #{Float.round(stats.active, 4)}g/l ", else: "")
           <> (if stats.active30m > 0 || stats.active1h > 0, do: "(15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) ", else: "")
           <> (if stats.sober_in > 0, do: "— Sobre dans #{stats.sober_in_s} ", else: "")
           <> (if stats.since && stats.since_min > 180, do: "— Paitai depuis #{stats.since_s} ", else: "")
           <> "— Dernier verre: #{present_type(stats.last_type, stats.last_descr)} [#{Float.round(stats.last_points+0.0, 4)}] "
           <> "#{format_duration_from_now(stats.last_at)} "
           <> (if stats.daily_volumes > 0, do: "— Aujourd'hui: #{stats.daily_volumes} #{stats.daily_gl}g/l", else: "")
 
           m.replyfun.(msg)
         else
           m.replyfun.("honteux mais #{nick} n'a pas l'air alcoolique du tout. /kick")
         end
       end
       else
       m.replyfun.("je ne connais pas cet utilisateur")
     end
     {:noreply, state}
   end
 
 
   # Account merge
   def handle_info({:account_change, old_id, new_id}, state) do
     spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
     Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
         Logger.debug("alcolog/account_change:: merging #{old_id} -> #{new_id}")
       rename_object_owner(table, state.ets, obj, old_id, new_id)
     end)
     case :dets.lookup(state.meta, {:meta, old_id}) do
       [{_, meta}] ->
         :dets.delete(state.meta, {:meta, old_id})
         :dets.insert(state.meta, {{:meta, new_id}, meta})
       _ ->
         :ok
     end
     {:noreply, state}
   end
 
   def terminate(_, state) do
     for dets <- [state.dets, state.meta] do
       :dets.sync(dets)
       :dets.close(dets)
     end
   end
 
   defp rename_object_owner(table, ets, object = {old_id, date, volume, current, cl, deg, name, comment, meta}, old_id, new_id) do
     :dets.delete_object(table, object)
     :ets.delete(ets, {old_id, date})
     :dets.insert(table, {new_id, date, volume, current, cl, deg, name, comment, meta})
     :ets.insert(ets, {{new_id, date}, volume, current, cl, deg, name, comment, meta})
   end
 
   # Account: move from nick to account id
   def handle_info({:accounts, accounts}, state) do
     #for x={:account, _, _, _, _} <- accounts, do: handle_info(x, state)
     #{:noreply, state}
     mapping = Enum.reduce(accounts, Map.new, fn({:account, _net, _chan, nick, account_id}, acc) ->
       Map.put(acc, String.downcase(nick), account_id)
     end)
     spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [], [:"$_"]}]
       Logger.debug("accounts:: mappings #{inspect mapping}")
     Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj = {nick, _date, _vol, _cur, _cl, _deg, _name, _comment, _meta}) ->
       #Logger.debug("accounts:: item #{inspect(obj)}")
       if new_id = Map.get(mapping, nick) do
         Logger.debug("alcolog/accounts:: merging #{nick} -> #{new_id}")
         rename_object_owner(table, state.ets, obj, nick, new_id)
       end
     end)
     {:noreply, state}
   end
 
   def handle_info({:account, _net, _chan, nick, account_id}, state) do
     nick = String.downcase(nick)
     spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}]
     Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
       Logger.debug("alcoolog/account:: merging #{nick} -> #{account_id}")
       rename_object_owner(table, state.ets, obj, nick, account_id)
     end)
     case :dets.lookup(state.meta, {:meta, nick}) do
       [{_, meta}] ->
         :dets.delete(state.meta, {:meta, nick})
         :dets.insert(state.meta, {{:meta, account_id}, meta})
       _ ->
         :ok
     end
     {:noreply, state}
   end
 
   def handle_info(t, state) do
     Logger.debug("#{__MODULE__}: unhandled info #{inspect t}")
     {:noreply, state}
   end
 
   def nick_history(account) do
     spec = [
       {{{:"$1", :_}, :_, :_, :_, :_, :_, :_, :_},
       [{:==, :"$1", {:const, account.id}}],
       [:"$_"]}
     ]
     :ets.select(data_state().ets, spec)
   end
 
   defp get_statistics_for_nick(state, account_id) do
     qvc = :dets.lookup(state.dets, account_id)
           |> Enum.sort_by(fn({_, ts, _, _, _, _, _, _, _}) -> ts end, &</2)
     count = Enum.reduce(qvc, 0, fn({_nick, _ts, points, _active, _cl, _deg, _type, _descr, _meta}, acc) -> acc + (points||0) end)
     last = List.last(qvc) || nil
     {count, last}
   end
 
   def present_type(type, descr) when descr in [nil, ""], do: "#{type}"
   def present_type(type, description), do: "#{type} (#{description})"
 
   def format_points(int) when is_integer(int) and int > 0 do
     "+#{Integer.to_string(int)}"
   end
   def format_points(int) when is_integer(int) and int < 0 do
     Integer.to_string(int)
   end
   def format_points(int) when is_float(int) and int > 0 do
     "+#{Float.to_string(Float.round(int,4))}"
   end
   def format_points(int) when is_float(int) and int < 0 do
     Float.to_string(Float.round(int,4))
   end
   def format_points(0), do: "0"
   def format_points(0.0), do: "0"
 
   defp format_relative_timestamp(timestamp) do
     alias Timex.Format.DateTime.Formatters
     alias Timex.Timezone
     date = timestamp
     |> DateTime.from_unix!(:millisecond)
     |> Timezone.convert("Europe/Paris")
 
     {:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
     {:ok, detail} = Formatters.Default.lformat(date, " ({h24}:{m})", "fr")
 
     relative <> detail
   end
 
   defp put_user_meta(state, account_id, meta) do
     :dets.insert(state.meta, {{:meta, account_id}, meta})
     :ok
   end
 
   defp get_user_meta(%{meta: meta}, account_id) do
     case :dets.lookup(meta, {:meta, account_id}) do
       [{{:meta, _}, meta}] ->
         Map.merge(@default_user_meta, meta)
       _ ->
         @default_user_meta
     end
   end
   # Calcul g/l actuel:
   # 1. load user meta
   # 2. foldr ets
   #   for each object
   #     get_current_alcohol
   #         ((object g/l) - 0,15/l/60)* minutes_since_drink
   #         if minutes_since_drink < 10, reduce g/l (?!)
   #     acc + current_alcohol
   #   stop folding when ?
   #
 
   def user_stats(account) do
     user_stats(data_state(), account.id)
   end
 
   defp user_stats(state = %{ets: ets}, account_id) do
     meta = get_user_meta(state, account_id)
     aday = (10 * 60)*60
     now = DateTime.utc_now()
     before = now
     |> DateTime.add(-aday, :second)
     |> DateTime.to_unix(:millisecond)
     #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
     match = [
       {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
        [
          {:>, :"$2", {:const, before}},
          {:"=:=", {:const, account_id}, :"$1"}
        ], [:"$_"]}
     ]
   # tuple ets:  {{nick, date}, volumes, current, nom, commentaire}
     drinks = :ets.select(ets, match)
     # {date, single_peak}
     total_volume = Enum.reduce(drinks, 0.0, fn({{_, date}, volume, _, _, _, _, _, _}, acc) ->
       acc + volume
     end)
     k = if meta.sex, do: 0.7, else: 0.6
     weight = meta.weight
     gl = (10*total_volume)/(k*weight)
     {Float.round(total_volume + 0.0, 4), Float.round(gl + 0.0, 4)}
   end
 
   defp alcohol_level_rising(state, account_id, minutes \\ 30) do
     {now, _} = current_alcohol_level(state, account_id)
     soon_date = DateTime.utc_now
                 |> DateTime.add(minutes*60, :second)
     {soon, _} = current_alcohol_level(state, account_id, soon_date)
     soon = cond do
       soon < 0 -> 0.0
       true -> soon
     end
     #IO.puts "soon #{soon_date} - #{inspect soon} #{inspect now}"
     {soon > now, Float.round(soon+0.0, 4)}
   end
 
   defp current_alcohol_level(state = %{ets: ets}, account_id, now \\ nil) do
     meta = get_user_meta(state, account_id)
     aday = ((24*7) * 60)*60
     now = if now do
       now
     else
       DateTime.utc_now()
     end
     before = now
     |> DateTime.add(-aday, :second)
     |> DateTime.to_unix(:millisecond)
     #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
     match = [
       {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
        [
          {:>, :"$2", {:const, before}},
          {:"=:=", {:const, account_id}, :"$1"}
        ], [:"$_"]}
     ]
   # tuple ets:  {{nick, date}, volumes, current, nom, commentaire}
     drinks = :ets.select(ets, match)
              |> Enum.sort_by(fn({{_, date}, _, _, _, _, _, _, _}) -> date end, &</2)
     # {date, single_peak}
     {all, last_drink_at, gl, active_drinks} = Enum.reduce(drinks, {0.0, nil, [], 0}, fn({{_, date}, volume, _, _, _, _, _, _}, {all, last_at, acc, active_drinks}) ->
       k = if meta.sex, do: 0.7, else: 0.6
       weight = meta.weight
       peak = (10*volume)/(k*weight)
       date = case date do
                ts when is_integer(ts) -> DateTime.from_unix!(ts, :millisecond)
                date = %NaiveDateTime{} -> DateTime.from_naive!(date, "Etc/UTC")
                date = %DateTime{} -> date
              end
       last_at = last_at || date
       mins_since = round(DateTime.diff(now, date)/60.0)
       #IO.puts "Drink: #{inspect({date, volume})} - mins since: #{inspect mins_since} - last drink at #{inspect last_at}"
       # Apply loss since `last_at` on `all`
       #
       all = if last_at do
         mins_since_last = round(DateTime.diff(date, last_at)/60.0)
         loss = ((meta.loss_factor/100)/60)*(mins_since_last)
         #IO.puts "Applying last drink loss: from #{all}, loss of #{inspect loss} (mins since #{inspect mins_since_first})"
         cond do
           (all-loss) > 0 -> all - loss
           true -> 0.0
         end
       else
         all
       end
             #IO.puts "Applying last drink current before drink: #{inspect all}"
       if mins_since < 30 do
         per_min = (peak)/30.0
         current = (per_min*mins_since)
           #IO.puts "Applying current drink 30m: from #{peak}, loss of #{inspect per_min}/min (mins since #{inspect mins_since})"
         {all + current, date, [{date, current} | acc], active_drinks + 1}
       else
         {all + peak, date, [{date, peak} | acc], active_drinks}
       end
     end)
     #IO.puts "last drink #{inspect last_drink_at}"
     mins_since_last = if last_drink_at do
       round(DateTime.diff(now, last_drink_at)/60.0)
     else
       0
     end
     # Si on a déjà bu y'a déjà moins 15 minutes (big up le binge drinking), on applique plus de perte
     level = if mins_since_last > 15 do
       loss = ((meta.loss_factor/100)/60)*(mins_since_last)
       Float.round(all - loss, 4)
     else
       all
     end
     #IO.puts "\n LEVEL #{inspect level}\n\n\n\n"
     cond do
       level < 0 -> {0.0, 0}
       true -> {level, active_drinks}
     end
   end
 
   defp format_duration_from_now(date, with_detail \\ true) do
     date = if is_integer(date) do
       date = DateTime.from_unix!(date, :millisecond)
       |> Timex.Timezone.convert("Europe/Paris")
     else
       Util.to_naive_date_time(date)
     end
     now = DateTime.utc_now()
     |> Timex.Timezone.convert("Europe/Paris")
     {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(date, "({h24}:{m})", "fr")
 
     mins_since = round(DateTime.diff(now, date)/60.0)
     if ago = format_minute_duration(mins_since) do
       word = if mins_since > 0 do
         "il y a "
       else
         "dans "
       end
       word <> ago <> if(with_detail, do: " #{detail}", else: "")
     else
       "maintenant #{detail}"
     end
   end
 
   defp format_minute_duration(minutes) do
     sober_in_s = if (minutes != 0) do
       duration = Timex.Duration.from_minutes(minutes)
       Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
     else
       nil
     end
   end
 
 end
diff --git a/lib/plugins/alcoolog_announcer.ex b/lib/plugins/alcoolog_announcer.ex
index 2baa999..9f66799 100644
--- a/lib/plugins/alcoolog_announcer.ex
+++ b/lib/plugins/alcoolog_announcer.ex
@@ -1,269 +1,269 @@
 defmodule Nola.Plugins.AlcoologAnnouncer do
   require Logger
 
   @moduledoc """
   Annonce changements d'alcoolog
   """
 
   @channel "#dmz"
 
   @seconds 30
 
   @apero [
     "C'EST L'HEURE DE L'APÉRRROOOOOOOO !!",
     "SAAAAANNNNNNNTTTTTTTTAAAAAAAAIIIIIIIIIIIIIIIIIIIIII",
     "APÉRO ? APÉRO !",
     {:timed, ["/!\\ à vos tires bouchons…", "… débouchez …", "… BUVEZ! SANTAIII!"]},
     "/!\\ ALERTE APÉRO /!\\",
     "CED !!! VASE DE ROUGE !",
     "DIDI UN PETIT RICARD™??!",
     "ALLEZ GUIGUI UNE PETITE BIERE ?",
     {:timed, ["/!\\ à vos tires bouchons…", "… débouchez …", "… BUVEZ! SANTAIII!"]},
     "APPPPAIIIRRREAAUUUUUUUUUUU"
   ]
 
   def irc_doc, do: nil
 
   def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
 
   def log(account) do
     dets_filename = (Nola.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist
     {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
     from = ~U[2020-08-23 19:41:40.524154Z]
     to = ~U[2020-08-24 19:41:40.524154Z]
     select = [
   {{:"$1", :"$2", :_},
    [
      {:andalso,
       {:andalso, {:==, :"$1", {:const, account.id}},
        {:>, :"$2", {:const, DateTime.to_unix(from)}}},
       {:<, :"$2", {:const, DateTime.to_unix(to)}}}
    ], [:"$_"]}
     ]
     res = :dets.select(dets, select)
     :dets.close(dets)
     res
   end
 
   def init(_) do
     {:ok, _} = Registry.register(Nola.PubSub, "account", [])
     stats = get_stats()
     Process.send_after(self(), :stats, :timer.seconds(30))
     dets_filename = (Nola.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist
     {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
     ets = nil # :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
     {:ok, {stats, now(), dets, ets}}#, {:continue, :traverse}}
   end
 
   def handle_continue(:traverse, state = {_, _, dets, ets}) do
     traverse_fun = fn(obj, dets) ->
       case obj do
         {nick, %DateTime{} = dt, active} ->
           :dets.delete_object(dets, obj)
           :dets.insert(dets, {nick, DateTime.to_unix(dt), active})
           IO.puts("ok #{inspect obj}")
           dets
         {nick, ts, value} ->
           :ets.insert(ets, { {nick, ts}, value })
           dets
       end
     end
     :dets.foldl(traverse_fun, dets, dets)
     :dets.sync(dets)
     IO.puts("alcoolog announcer fixed")
     {:noreply, state}
   end
 
   def alcohol_reached(old, new, level) do
       (old.active < level && new.active >= level) && (new.active5m >= level)
     end
 
   def alcohol_below(old, new, level) do
       (old.active > level && new.active <= level) && (new.active5m <= level)
     end
 
 
   def handle_info(:stats, {old_stats, old_now, dets, ets}) do
     stats = get_stats()
     now = now()
 
     if old_now.hour < 18 && now.hour == 18 do
       apero = Enum.shuffle(@apero)
               |> Enum.random()
 
       case apero do
         {:timed, list} ->
           spawn(fn() ->
             for line <- list do
               IRC.Connection.broadcast_message("evolu.net", "#dmz", line)
               :timer.sleep(:timer.seconds(5))
             end
           end)
         string ->
           IRC.Connection.broadcast_message("evolu.net", "#dmz", string)
       end
 
     end
 
     #IO.puts "newstats #{inspect stats}"
     events = for {acct, old} <- old_stats do
       new = Map.get(stats, acct, nil)
       #IO.puts "#{acct}: #{inspect(old)} -> #{inspect(new)}"
 
       now = DateTime.to_unix(DateTime.utc_now())
       if new && new[:active] do
         :dets.insert(dets, {acct, now, new[:active]})
         :ets.insert(ets, {{acct, now}, new[:active]})
       else
         :dets.insert(dets, {acct, now, 0.0})
         :ets.insert(ets, {{acct, now}, new[:active]})
       end
 
       event = cond do
         old == nil -> nil
         (old.active > 0) && (new == nil) -> :sober
         new == nil -> nil
         alcohol_reached(old, new, 0.5) -> :stopconduire
         alcohol_reached(old, new, 1.0) -> :g1
         alcohol_reached(old, new, 2.0) -> :g2
         alcohol_reached(old, new, 3.0) -> :g3
         alcohol_reached(old, new, 4.0) -> :g4
         alcohol_reached(old, new, 5.0) -> :g5
         alcohol_reached(old, new, 6.0) -> :g6
         alcohol_reached(old, new, 7.0) -> :g7
         alcohol_reached(old, new, 10.0) -> :g10
         alcohol_reached(old, new, 13.74) -> :record
         alcohol_below(old, new, 0.5) -> :conduire
         alcohol_below(old, new, 1.0) -> :fini1g
         alcohol_below(old, new, 2.0) -> :fini2g
         alcohol_below(old, new, 3.0) -> :fini3g
         alcohol_below(old, new, 4.0) -> :fini4g
         (old.rising) && (!new.rising) -> :lowering
         true -> nil
       end
       {acct, event}
     end
 
     for {acct, event} <- events do
       message = case event do
         :g1 -> [
             "[vigicuite jaune] LE GRAMME! LE GRAMME O/",
             "début de vigicuite jaune ! LE GRAMME ! \\O/",
             "waiiiiiiii le grammmeee",
             "bourraiiiiiiiiiiide 1 grammeeeeeeeeeee",
           ]
         :g2 -> [
             "[vigicuite orange] \\o_YAY 2 GRAMMES ! _o/",
             "PAITAIIIIIIIIII DEUX GRAMMEESSSSSSSSSSSSSSSSS",
             "bourrrrrraiiiiiiiiiiiiiiiide 2 grammeeeeeeeeeees",
           ]
         :g3 -> [
             "et un ! et deux ! et TROIS GRAMMEEESSSSSSS",
             "[vigicuite rouge] _o/ BOURRAIIDDDEEEE 3 GRAMMESSSSSSSSS \\o/ \\o/"
           ]
         :g4 -> [
             "[vigicuite écarlate] et un, et deux, et trois, ET QUATRES GRAMMEESSSSSSSSSSSSSSSSSSSssssss"
           ]
         :g5 -> "[vigicuite écarlate+] PUTAIN 5 GRAMMES !"
         :g6 -> "[vigicuite écarlate++] 6 grammes ? Vous pouvez joindre Alcool info service au 0 980 980 930"
         :g7 -> "[vigicuite c'est la merde] 7 grammes. Le SAMU, c'est le 15."
         :g10 -> "BORDLE 10 GRAMMES"
         :record -> "RECORD DU MONDE BATTU ! >13.74g/l !!"
         :fini1g -> [
             "fin d'alerte vigicuite jaune, passage en vert (<1g/l)",
             "/!\\ alerte moins de 1g/l /!\\"
           ]
         :fini2g -> [
             "t'as moins de 2 g/l, faut se reprendre là [vigicuite jaune]"
           ]
         :fini3g -> [
             "fin d'alerte vigicuite rouge, passage en orange (<3g/l)"
           ]
         :fini4g -> [
             "fin d'alerte vigicuite écarlate, passage en rouge (<4g/l)"
           ]
         :lowering -> [
             "attention ça baisse!",
             "tu vas quand même pas en rester là ?",
             "IL FAUT CONTINUER À BOIRE !",
             "t'abandonnes déjà ?",
             "!santai ?",
             "faut pas en rester là",
             "il faut se resservir",
             "coucou faut reboire",
             "encore un petit verre ?",
             "abwaaaaaaaaaaaaarrrrrrrrrrrrrr",
             "taux d'alcoolémie en chute ! agissez avant qu'il soit trop tard!",
             "ÇA BAISSE !!"
         ]
         :stopconduire -> [
             "0.5g! bientot le gramme?",
             "tu peux plus prendre la route... mais... tu peux prendre la route du gramme! !santai !",
             "fini la conduite!",
             "0.5! continues faut pas en rester là!",
             "beau début, continues !",
             "ça monte! 0.5g/l!"
         ]
         :conduire -> [
             "tu peux conduire, ou recommencer à boire! niveau critique!",
             "!santai ?",
             "tu peux reprendre la route, ou reprendre la route du gramme..",
             "attention, niveau critique!",
             "il faut boire !!",
             "trop de sang dans ton alcool, c'est mauvais pour la santé",
             "faut pas en rester là !",
           ]
         :sober -> [
             "sobre…",
             "/!\\ alerte sobriété /!\\",
             "... sobre?!?!",
             "sobre :(",
             "attention, t'es sobre :/",
             "danger, alcoolémie à 0.0 !",
             "sobre! c'était bien on recommence quand ?",
             "sobre ? Faut recommencer...",
             "T'es sobre. Ne te laisses pas abattre- ton caviste peut aider.",
             "Vous êtes sobre ? Ceci n'est pas une fatalité - resservez vous vite !"
         ]
         _ -> nil
       end
       message = case message do
         m when is_binary(m) -> m
         m when is_list(m) -> m |> Enum.shuffle() |> Enum.random()
         nil -> nil
       end
       if message do
         #IO.puts("#{acct}: #{message}")
         account = Nola.Account.get(acct)
-        for {net, chan} <- IRC.Membership.notify_channels(account) do
+        for {net, chan} <- Nola.Membership.notify_channels(account) do
           user = IRC.UserTrack.find_by_account(net, account)
           nick = if(user, do: user.nick, else: account.name)
           IRC.Connection.broadcast_message(net, chan, "#{nick}: #{message}")
         end
       end
     end
 
     timer()
 
     #IO.puts "tick stats ok"
     {:noreply, {stats,now,dets,ets}}
   end
 
   def handle_info(_, state) do
     {:noreply, state}
   end
 
   defp now() do
     DateTime.utc_now()
     |> Timex.Timezone.convert("Europe/Paris")
   end
 
   defp get_stats() do
     Enum.into(Nola.Plugins.Alcoolog.get_all_stats(), %{})
   end
 
   defp timer() do
     Process.send_after(self(), :stats, :timer.seconds(@seconds))
   end
 
 end
diff --git a/lib/plugins/last_fm.ex b/lib/plugins/last_fm.ex
index 4607cbe..68c55ee 100644
--- a/lib/plugins/last_fm.ex
+++ b/lib/plugins/last_fm.ex
@@ -1,187 +1,187 @@
 defmodule Nola.Plugins.LastFm do
   require Logger
 
   @moduledoc """
   # last.fm
 
   * **!lastfm|np `[nick|username]`**
   * **.lastfm|np**
   * **+lastfm, -lastfm `<username last.fm>; ?lastfm`** Configurer un nom d'utilisateur last.fm
   """
 
   @single_trigger ~w(lastfm np)
   @pubsub_topics ~w(trigger:lastfm trigger:np)
 
   defstruct dets: nil
 
   def irc_doc, do: @moduledoc
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def init([]) do
     regopts = [type: __MODULE__]
     for t <- @pubsub_topics, do: {:ok, _} = Registry.register(Nola.PubSub, t, type: __MODULE__)
     dets_filename = (Nola.data_path() <> "/" <> "lastfm.dets") |> String.to_charlist
     {:ok, dets} = :dets.open_file(dets_filename, [])
     {:ok, %__MODULE__{dets: dets}}
   end
 
   def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :plus, args: [username]}}}, state) do
     username = String.strip(username)
     :ok = :dets.insert(state.dets, {message.account.id, username})
     message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm configuré: \"#{username}\".")
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :minus, args: []}}}, state) do
     text = case :dets.lookup(state.dets, message.account.id) do
              [{_nick, _username}] ->
                :dets.delete(state.dets, message.account.id)
                message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm enlevé.")
              _ -> nil
            end
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :query, args: []}}}, state) do
     text = case :dets.lookup(state.dets, message.account.id) do
       [{_nick, username}] ->
         message.replyfun.("#{message.sender.nick}: #{username}.")
       _ -> nil
     end
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: []}}}, state) do
     irc_now_playing(message.account.id, message, state)
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: [nick_or_user]}}}, state) do
     irc_now_playing(nick_or_user, message, state)
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :dot}}}, state) do
-    members = IRC.Membership.members(message.network, message.channel)
+    members = Nola.Membership.members(message.network, message.channel)
     foldfun = fn({nick, user}, acc) -> [{nick,user}|acc] end
     usernames = :dets.foldl(foldfun, [], state.dets)
                 |> Enum.uniq()
                 |> Enum.filter(fn({acct,_}) -> Enum.member?(members, acct) end)
                 |> Enum.map(fn({_, u}) -> u end)
     for u <- usernames, do: irc_now_playing(u, message, state)
     {:noreply, state}
   end
 
   def handle_info(info, state) do
     {:noreply, state}
   end
 
   def terminate(_reason, state) do
     if state.dets do
       :dets.sync(state.dets)
       :dets.close(state.dets)
     end
     :ok
   end
 
   defp irc_now_playing(nick_or_user, message, state) do
     nick_or_user = String.strip(nick_or_user)
 
     id_or_user = if account = Nola.Account.get(nick_or_user) || Nola.Account.find_always_by_nick(message.network, message.channel, nick_or_user) do
       account.id
     else
       nick_or_user
     end
 
     username = case :dets.lookup(state.dets, id_or_user) do
       [{_, username}] -> username
       _ -> id_or_user
     end
 
     case now_playing(username) do
       {:error, text} when is_binary(text) ->
         message.replyfun.(text)
       {:ok, map} when is_map(map) ->
         track = fetch_track(username, map)
         text = format_now_playing(map, track)
         user = if account = Nola.Account.get(id_or_user) do
           user = IRC.UserTrack.find_by_account(message.network, account)
           if(user, do: user.nick, else: account.name)
         else
           username
         end
         if user && text do
           message.replyfun.("#{user} #{text}")
         else
           message.replyfun.("#{username}: pas de résultat")
         end
       other ->
         message.replyfun.("erreur :(")
     end
   end
 
   defp now_playing(user) do
     api = Application.get_env(:nola, :lastfm)[:api_key]
     url = "http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&format=json&limit=1&extended=1" <> "&api_key=" <> api <> "&user="<> user
     case HTTPoison.get(url) do
       {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> Jason.decode(body)
       {:ok, %HTTPoison.Response{status_code: 404}} -> {:error, "last.fm: utilisateur \"#{user}\" inexistant"}
       {:ok, %HTTPoison.Response{status_code: code}} -> {:error, "last.fm: erreur #{to_string(code)}"}
       error ->
         Logger.error "Lastfm http error: #{inspect error}"
         :error
     end
   end
   defp fetch_track(user, %{"recenttracks" => %{"track" => [ t = %{"name" => name, "artist" => %{"name" => artist}} | _]}}) do
     api = Application.get_env(:nola, :lastfm)[:api_key]
     url = "http://ws.audioscrobbler.com/2.0/?method=track.getInfo&format=json" <> "&api_key=" <> api <> "&username="<> user <> "&artist="<>URI.encode(artist)<>"&track="<>URI.encode(name)
     case HTTPoison.get(url) do
       {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
         case Jason.decode(body) do
           {:ok, body} -> body["track"] || %{}
           _ -> %{}
         end
       error ->
         Logger.error "Lastfm http error: #{inspect error}"
         :error
     end
   end
 
   defp format_now_playing(%{"recenttracks" => %{"track" => [track = %{"@attr" => %{"nowplaying" => "true"}} | _]}}, et) do
     format_track(true, track, et)
   end
 
   defp format_now_playing(%{"recenttracks" => %{"track" => [track | _]}}, et) do
     format_track(false, track, et)
   end
 
   defp format_now_playing(%{"error" => err, "message" => message}, _) do
     "last.fm error #{err}: #{message}"
   end
 
   defp format_now_playing(miss) do
     nil
   end
 
   defp format_track(np, track, extended) do
     artist = track["artist"]["name"]
     album = if track["album"]["#text"], do: " (" <> track["album"]["#text"] <> ")", else: ""
     name = track["name"] <> album
     action = if np, do: "écoute ", else: "a écouté"
     love = if track["loved"] != "0", do: "❤️"
     count = if x = extended["userplaycount"], do: "x#{x} #{love}"
     tags = (get_in(extended, ["toptags", "tag"]) || [])
     |> Enum.map(fn(tag) -> tag["name"] end)
     |> Enum.filter(& &1)
     |> Enum.join(", ")
 
     [action, artist, name, count, tags, track["url"]]
     |> Enum.filter(& &1)
     |> Enum.map(&String.trim(&1))
     |> Enum.join(" - ")
   end
 
 end
diff --git a/lib/plugins/say.ex b/lib/plugins/say.ex
index e036bd4..9bfe1bd 100644
--- a/lib/plugins/say.ex
+++ b/lib/plugins/say.ex
@@ -1,73 +1,73 @@
 defmodule Nola.Plugins.Say do
 
   def irc_doc do
     """
     # say
 
     Say something...
 
     * **!say `<channel>` `<text>`** say something on `channel`
     * **!asay `<channel>` `<text>`** same but anonymously
 
     You must be a member of the channel.
     """
   end
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def init([]) do
     regopts = [type: __MODULE__]
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:say", regopts)
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:asay", regopts)
     {:ok, _} = Registry.register(Nola.PubSub, "messages:private", regopts)
     {:ok, nil}
   end
 
   def handle_info({:irc, :trigger, "say", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do
     text = Enum.join(text, " ")
     say_for(m.account, target, text, true)
     {:noreply, state}
    end
 
   def handle_info({:irc, :trigger, "asay", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do
     text = Enum.join(text, " ")
     say_for(m.account, target, text, false)
     {:noreply, state}
    end
 
   def handle_info({:irc, :text, m = %{text: "say "<>rest}}, state) do
     case String.split(rest, " ", parts: 2) do
       [target, text] -> say_for(m.account, target, text, true)
       _ -> nil
     end
     {:noreply, state}
   end
 
   def handle_info({:irc, :text, m = %{text: "asay "<>rest}}, state) do
     case String.split(rest, " ", parts: 2) do
       [target, text] -> say_for(m.account, target, text, false)
       _ -> nil
     end
     {:noreply, state}
   end
 
   def handle_info(_, state) do
     {:noreply, state}
   end
 
   defp say_for(account, target, text, with_nick?) do
-    for {net, chan} <- IRC.Membership.of_account(account) do
+    for {net, chan} <- Nola.Membership.of_account(account) do
       chan2 = String.replace(chan, "#", "")
       if (target == "#{net}/#{chan}" || target == "#{net}/#{chan2}" || target == chan || target == chan2) do
         if with_nick? do
           IRC.send_message_as(account, net, chan, text)
         else
           IRC.Connection.broadcast_message(net, chan, text)
         end
       end
     end
   end
 
 end
diff --git a/lib/telegram/telegram.ex b/lib/telegram/telegram.ex
index a93f5bb..dd23146 100644
--- a/lib/telegram/telegram.ex
+++ b/lib/telegram/telegram.ex
@@ -1,233 +1,233 @@
 defmodule Nola.Telegram do
   require Logger
   @behaviour Telegram.ChatBot
 
   def my_path() do
     "https://t.me/beauttebot"
   end
 
   def send_message(id, text, md2 \\ false) do
     md = if md2, do: "MarkdownV2", else: "Markdown"
     token = Keyword.get(Application.get_env(:nola, :telegram, []), :key)
     Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(Nola.Telegram, id)
     Telegram.Api.request(token, "sendMessage", chat_id: id, text: text, parse_mode: "Markdown")
   end
 
   @impl Telegram.ChatBot
   def init(chat_id) when chat_id < 0 do
     {:ok, state} = Nola.TelegramRoom.init(chat_id)
     {:ok, %{room_state: state}}
   end
   def init(chat_id) do
     Logger.info("Telegram session starting: #{chat_id}")
     account = Nola.Account.find_meta_account("telegram-id", chat_id)
     account_id = if account, do: account.id
     {:ok, %{account: account_id}}
   end
 
   @impl Telegram.ChatBot
   def handle_update(update, token, %{room_state: room_state}) do
     {:ok, room_state} = Nola.TelegramRoom.handle_update(update, token, room_state)
     {:ok, %{room_state: room_state}}
   end
 
   def handle_update(%{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/start"<>_}}, _token, state) do
     text = "*Welcome to beautte!*\n\nQuery the bot on IRC and say \"enable-telegram\" to continue."
     send_message(m["chat"]["id"], text)
     {:ok, %{account: nil}}
   end
 
   def handle_update(%{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/enable"<>_}}, _token, state) do
     key = case String.split(text, " ") do
       ["/enable", key | _] -> key
       _ -> "nil"
     end
 
     #Handled message "1247435154:AAGnSSCnySn0RuVxy_SUcDEoOX_rbF6vdq0" %{"message" =>
     #  %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
     #    "date" => 1591027272, "entities" =>
     #    [%{"length" => 7, "offset" => 0, "type" => "bot_command"}],
     #    "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
     #    "message_id" => 11, "text" => "/enable salope"}, "update_id" => 764148578}
     account = Nola.Account.find_meta_account("telegram-validation-code", String.downcase(key))
     text = if account do
       net = Nola.Account.get_meta(account, "telegram-validation-target")
       Nola.Account.put_meta(account, "telegram-id", m["chat"]["id"])
       Nola.Account.put_meta(account, "telegram-username", m["chat"]["username"])
       Nola.Account.put_meta(account, "telegram-username", m["chat"]["username"])
       Nola.Account.delete_meta(account, "telegram-validation-code")
       Nola.Account.delete_meta(account, "telegram-validation-target")
       IRC.Connection.broadcast_message(net, account, "Telegram #{m["chat"]["username"]} account added!")
       "Yay! Linked to account **#{account.name}**."
     else
       "Token invalid"
     end
     send_message(m["chat"]["id"], text)
     {:ok, %{account: account.id}}
   end
 
   #[debug] Unhandled update: %{"message" =>
   #  %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
   #    "date" => 1591096015,
   #    "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
   #    "message_id" => 29,
   #    "photo" => [
   #      %{"file_id" => "AgACAgQAAxkBAAMdXtYyz4RQqLcpOlR6xKK3w3NayHAAAnCzMRuL4bFSgl_cTXMl4m5G_T0kXQADAQADAgADbQADZVMBAAEaBA",
   #        "file_size" => 9544, "file_unique_id" => "AQADRv09JF0AA2VTAQAB", "height" => 95, "width" => 320},
   #      %{"file_id" => "AgACAgQAAxkBAAMdXtYyz4RQqLcpOlR6xKK3w3NayHAAAnCzMRuL4bFSgl_cTXMl4m5G_T0kXQADAQADAgADeAADZFMBAAEaBA",
   #        "file_size" => 21420, "file_unique_id" => "AQADRv09JF0AA2RTAQAB", "height" => 148, "width" => 501}]},
   #  "update_id" => 218161546}
 
   for type <- ~w(photo voice video document animation) do
     def handle_update(data = %{"message" => %{unquote(type) => _}}, token, state) do
       start_upload(unquote(type), data, token, state)
     end
   end
 
   #[debug] Unhandled update: %{"callback_query" =>
   #  %{
   #    "chat_instance" => "-7948978714441865930", "data" => "evolu.net/#dmz",
   #      "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
   #      "id" => "8913804780149600",
   #       "message" => %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
   #                    "date" => 1591098553, "from" => %{"first_name" => "devbeautte", "id" => 1293058838, "is_bot" => true, "username" => "devbeauttebot"},
   #                    "message_id" => 62,
   #                    "reply_markup" => %{"inline_keyboard" => [[%{"callback_data" => "random/#", "text" => "random/#"},
   #                          %{"callback_data" => "evolu.net/#dmz", "text" => "evolu.net/#dmz"}]]},
   #                    "text" => "Where should I send the file?"}
   #   }
   #  , "update_id" => 218161568}
 
   #def handle_update(t, %{"callback_query" => cb = %{"data" => "resend", "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}) do
   #end
 
   def handle_update(%{"callback_query" => cb = %{"data" => "start-upload:"<>target, "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}, t, state) do
     account = Nola.Account.find_meta_account("telegram-id", chat_id)
     if account do
       target = case String.split(target, "/") do
-        ["everywhere"] -> IRC.Membership.of_account(account)
+        ["everywhere"] -> Nola.Membership.of_account(account)
         [net, chan] -> [{net, chan}]
       end
         Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Processing...", reply_markup: %{})
 
       {content, type} = cond do
         op["photo"] -> {op["photo"], ""}
         op["voice"] -> {op["voice"], " a voice message"}
         op["video"] -> {op["video"], ""}
         op["document"] -> {op["document"], ""}
         op["animation"] -> {op["animation"], ""}
       end
 
       file = if is_list(content) && Enum.count(content) > 1 do
         Enum.sort_by(content, fn(p) -> p["file_size"] end, &>=/2)
         |> List.first()
       else
         content
       end
       file_id = file["file_id"]
       file_unique_id = file["file_unique_id"]
         text = if(op["caption"], do: ": "<> op["caption"] <> "", else: "")
         resend = %{"inline_keyboard" => [ [%{"text" => "re-share", "callback_data" => "resend"}] ]}
       spawn(fn() ->
         with \
           {:ok, file} <- Telegram.Api.request(t, "getFile", file_id: file_id),
           path = "https://api.telegram.org/file/bot#{t}/#{file["file_path"]}",
           {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path),
           <<smol_body::binary-size(20), _::binary>> = body,
           {:ok, magic} <- GenMagic.Pool.perform(Nola.GenMagic, {:bytes, smol_body}),
           bucket = Application.get_env(:nola, :s3, []) |> Keyword.get(:bucket),
           ext = Path.extname(file["file_path"]),
           s3path = "#{account.id}/#{file_unique_id}#{ext}",
           Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "*Uploading...*", reply_markup: %{}, parse_mode: "MarkdownV2"),
           s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: magic.mime_type),
           {:ok, _} <- ExAws.request(s3req)
         do
           path = NolaWeb.Router.Helpers.url(NolaWeb.Endpoint) <> "/files/#{s3path}"
           sent = for {net, chan} <- target do
             txt = "sent#{type}#{text} #{path}"
             IRC.send_message_as(account, net, chan, txt)
             "#{net}/#{chan}"
           end
           if caption = op["caption"], do: as_irc_message(chat_id, caption, account)
           text = "Sent on " <> Enum.join(sent, ", ") <> " !"
           Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "_Sent!_", reply_markup: %{}, parse_mode: "MarkdownV2")
         else
           error ->
             Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Something failed.", reply_markup: %{}, parse_mode: "MarkdownV2")
             Logger.error("Failed upload from Telegram: #{inspect error}")
         end
       end)
     end
     {:ok, state}
   end
 
   def handle_update(%{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}, "text" => text}}, _, state) do
     account = Nola.Account.find_meta_account("telegram-id", id)
     if account do
       as_irc_message(id, text, account)
     end
     {:ok, state}
   end
 
   def handle_update(m, _, state) do
     Logger.debug("Unhandled update: #{inspect m}")
     {:ok, state}
   end
 
   @impl Telegram.ChatBot
   def handle_info(info, %{room_state: room_state}) do
     {:ok, room_state} = Nola.TelegramRoom.handle_info(info, room_state)
     {:ok, %{room_state: room_state}}
   end
 
   def handle_info(_info, state) do
     {:ok, state}
   end
 
   defp as_irc_message(id, text, account) do
     reply_fun = fn(text) -> send_message(id, text) end
     trigger_text = cond do
       String.starts_with?(text, "/") ->
         "/"<>text = text
         "!"<>text
       Enum.any?(IRC.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(text, trigger) end) ->
         text
       true ->
         "!"<>text
     end
       message = %IRC.Message{
         id: FlakeId.get(),
         transport: :telegram,
       network: "telegram",
       channel: nil,
       text: text,
       account: account,
       sender: %ExIRC.SenderInfo{nick: account.name},
       replyfun: reply_fun,
         trigger: IRC.Connection.extract_trigger(trigger_text),
         at: nil
     }
     IRC.Connection.publish(message, ["messages:private", "messages:telegram", "telegram/#{account.id}:messages"])
     message
   end
 
   defp start_upload(_type, %{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}}}, token, state) do
     account = Nola.Account.find_meta_account("telegram-id", id)
     if account do
       text = if(m["text"], do: m["text"], else: nil)
-      targets = IRC.Membership.of_account(account)
+      targets = Nola.Membership.of_account(account)
       |> Enum.map(fn({net, chan}) -> "#{net}/#{chan}" end)
       |> Enum.map(fn(i) -> %{"text" => i, "callback_data" => "start-upload:#{i}"} end)
       kb = if Enum.count(targets) > 1 do
         [%{"text" => "everywhere", "callback_data" => "start-upload:everywhere"}] ++ targets
       else
         targets
       end
       |> Enum.chunk_every(2)
       keyboard = %{"inline_keyboard" => kb}
       Telegram.Api.request(token, "sendMessage", chat_id: id, text: "Where should I send this file?", reply_markup: keyboard, reply_to_message_id: m["message_id"], parse_mode: "MarkdownV2")
     end
     {:ok, state}
   end
 
 end
diff --git a/lib/web/context_plug.ex b/lib/web/context_plug.ex
index 7289763..fcdf42f 100644
--- a/lib/web/context_plug.ex
+++ b/lib/web/context_plug.ex
@@ -1,92 +1,92 @@
 defmodule NolaWeb.ContextPlug do
   import Plug.Conn
   import Phoenix.Controller
 
   def init(opts \\ []) do
     opts || []
   end
 
   def get_account(conn) do
     cond do
       get_session(conn, :account) -> get_session(conn, :account)
       get_session(conn, :oidc_id) -> if account = Nola.Account.find_meta_account("identity-id", get_session(conn, :oidc_id)), do: account.id
       true -> nil
     end
   end
 
   def call(conn, opts) do
     account = with \
          {:account, account_id} when is_binary(account_id) <- {:account, get_account(conn)},
          {:account, account} when not is_nil(account) <- {:account, Nola.Account.get(account_id)}
     do
       account
     else
       _ -> nil
     end
 
     network = Map.get(conn.params, "network")
     network = if network == "-", do: nil, else: network
 
     oidc_account = Nola.Account.find_meta_account("identity-id", get_session(conn, :oidc_id))
 
     conns = IRC.Connection.get_network(network)
     chan = if c = Map.get(conn.params, "chan") do
       NolaWeb.reformat_chan(c)
     end
     chan_conn = IRC.Connection.get_network(network, chan)
 
     memberships = if account do
-      IRC.Membership.of_account(account)
+      Nola.Membership.of_account(account)
     end
 
     auth_required = cond do
       Keyword.get(opts, :restrict) == :public -> false
       account == nil -> true
       network == nil -> false
       Keyword.get(opts, :restrict) == :logged_in -> false
       network && chan ->
         !Enum.member?(memberships, {network, chan})
       network ->
         !Enum.any?(memberships, fn({n, _}) -> n == network end)
     end
 
     bot = cond do
       network && chan && chan_conn -> chan_conn.nick
       network && conns -> conns.nick
       true -> nil
     end
 
 
     cond do
       account && auth_required ->
         conn
         |> put_status(404)
         |> text("Page not found")
         |> halt()
       auth_required ->
         conn
         |> put_status(403)
         |> render(NolaWeb.AlcoologView, "auth.html", bot: bot, no_header: true, network: network)
         |> halt()
       (network && !conns) ->
         conn
         |> put_status(404)
         |> text("Page not found")
         |> halt()
       (chan && !chan_conn) ->
         conn
         |> put_status(404)
         |> text("Page not found")
         |> halt()
       true ->
         conn = conn
            |> assign(:network, network)
            |> assign(:chan, chan)
            |> assign(:bot, bot)
            |> assign(:account, account)
            |> assign(:oidc_account, oidc_account)
            |> assign(:memberships, memberships)
     end
   end
 
 end
diff --git a/lib/web/controllers/alcoolog_controller.ex b/lib/web/controllers/alcoolog_controller.ex
index dc09517..6337da5 100644
--- a/lib/web/controllers/alcoolog_controller.ex
+++ b/lib/web/controllers/alcoolog_controller.ex
@@ -1,323 +1,323 @@
 defmodule NolaWeb.AlcoologController do
   use NolaWeb, :controller
   require Logger
 
   plug NolaWeb.ContextPlug when action not in [:token]
   plug NolaWeb.ContextPlug, [restrict: :public] when action in [:token]
 
   def token(conn, %{"token" => token}) do
     case Nola.Token.lookup(token) do
       {:ok, {:alcoolog, :index, network, channel}} -> index(conn, nil, network, channel)
       err ->
         Logger.debug("AlcoologControler: token #{inspect err} invalid")
         conn
         |> put_status(404)
         |> text("Page not found")
     end
   end
 
   def nick(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
     profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
     days = String.to_integer(Map.get(params, "days", "180"))
-    friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
+    friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
     if friend? do
       stats = Nola.Plugins.Alcoolog.get_full_statistics(profile_account.id)
       history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- Nola.Plugins.Alcoolog.nick_history(profile_account) do
         %{
           at: ts |> DateTime.from_unix!(:millisecond),
           points: points,
           active: active,
           cl: cl,
           deg: deg,
           type: type,
           description: descr,
           meta: meta
         }
     end
       history = Enum.sort(history, &(DateTime.compare(&1.at, &2.at) != :lt))
                 |> IO.inspect()
       conn
       |> assign(:title, "alcoolog #{nick}")
       |> render("user.html", network: network, profile: profile_account, days: days, nick: nick, history: history, stats: stats)
     else
       conn
       |> put_status(404)
       |> text("Page not found")
     end
   end
 
   def nick_stats_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
     profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
-    friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
+    friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
     if friend? do
       stats = Nola.Plugins.Alcoolog.get_full_statistics(profile_account.id)
 
       conn
       |> put_resp_content_type("application/json")
       |> text(Jason.encode!(stats))
     else
       conn
       |> put_status(404)
       |> json([])
     end
   end
 
   def nick_gls_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
     profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
-    friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
+    friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
     count = String.to_integer(Map.get(params, "days", "180"))
     if friend? do
       data = Nola.Plugins.Alcoolog.user_over_time_gl(profile_account, count)
     delay = count*((24 * 60)*60)
     now = DateTime.utc_now()
     start_date = DateTime.utc_now()
              |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
              |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
              |> DateTime.to_date()
              |> Date.to_erl()
   filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
   |> Enum.to_list
   |> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
   |> Enum.map(&Date.from_erl!(&1))
   |> Enum.map(fn(date) ->
     %{date: date, gls: Map.get(data, date, 0)}
   end)
   |> Enum.sort(&(Date.compare(&1.date, &2.date) != :gt))
 
       conn
       |> put_resp_content_type("application/json")
       |> text(Jason.encode!(filled))
     else
       conn
       |> put_status(404)
       |> json([])
     end
   end
 
 
 
   def nick_volumes_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
     profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
-    friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
+    friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
     count = String.to_integer(Map.get(params, "days", "180"))
     if friend? do
       data = Nola.Plugins.Alcoolog.user_over_time(profile_account, count)
     delay = count*((24 * 60)*60)
     now = DateTime.utc_now()
     start_date = DateTime.utc_now()
              |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
              |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
              |> DateTime.to_date()
              |> Date.to_erl()
   filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
   |> Enum.to_list
   |> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
   |> Enum.map(&Date.from_erl!(&1))
   |> Enum.map(fn(date) ->
     %{date: date, volumes: Map.get(data, date, 0)}
   end)
   |> Enum.sort(&(Date.compare(&1.date, &2.date) != :gt))
 
       conn
       |> put_resp_content_type("application/json")
       |> text(Jason.encode!(filled))
     else
       conn
       |> put_status(404)
       |> json([])
     end
   end
 
   def nick_log_json(conn = %{assigns: %{account: account}}, %{"network" => network, "nick" => nick}) do
     profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
-    friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
+    friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
     if friend? do
       history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- Nola.Plugins.Alcoolog.nick_history(profile_account) do
         %{
           at: ts |> DateTime.from_unix!(:millisecond) |> DateTime.to_iso8601(),
           points: points,
           active: active,
           cl: cl,
           deg: deg,
           type: type,
           description: descr,
           meta: meta
         }
     end
       last = List.last(history)
       {_, active} = Nola.Plugins.Alcoolog.user_stats(profile_account)
       last = %{last | active: active, at: DateTime.utc_now() |> DateTime.to_iso8601()}
       history = history ++ [last]
 
       conn
       |> put_resp_content_type("application/json")
       |> text(Jason.encode!(history))
     else
       conn
       |> put_status(404)
       |> json([])
     end
   end
 
   def nick_history_json(conn = %{assigns: %{account: account}}, %{"network" => network, "nick" => nick}) do
     profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
-    friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
+    friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
     if friend? do
       history = for {_, date, value} <- Nola.Plugs.AlcoologAnnouncer.log(profile_account) do
         %{date: DateTime.to_iso8601(date), value: value}
     end
       conn
       |> put_resp_content_type("application/json")
       |> text(Jason.encode!(history))
     else
       conn
       |> put_status(404)
       |> json([])
     end
   end
 
   def index(conn = %{assigns: %{account: account}}, %{"network" => network, "chan" => channel}) do
     index(conn, account, network, NolaWeb.reformat_chan(channel))
   end
 
   def index(conn = %{assigns: %{account: account}}, _) do
     index(conn, account, nil, nil)
   end
 
   #def index(conn, params) do
   #  network = Map.get(params, "network")
   #  chan = if c = Map.get(params, "chan") do
   #    NolaWeb.reformat_chan(c)
   #  end
   #  irc_conn = if network do
   #    IRC.Connection.get_network(network, chan)
   #  end
   #  bot = if(irc_conn, do: irc_conn.nick)#
   #
   #  conn
   #  |> put_status(403)
   #  |> render("auth.html", network: network, channel: chan, irc_conn: conn, bot: bot)
   #end
 
   def index(conn, account, network, channel) do
     aday = ((24 * 60)*60)
     now = DateTime.utc_now()
     before7 = now
              |> DateTime.add(-(7*aday), :second)
             |> DateTime.to_unix(:millisecond)
     before15 = now
              |> DateTime.add(-(15*aday), :second)
             |> DateTime.to_unix(:millisecond)
     before31 = now
              |> DateTime.add(-(31*aday), :second)
             |> DateTime.to_unix(:millisecond)
     #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
     match = [
   {{{:_, :"$1"}, :_, :_, :_, :_, :_, :_, :_},
    [
      {:>, :"$1", {:const, before15}},
    ], [:"$_"]}
     ]
 
   # tuple ets:  {{nick, date}, volumes, current, nom, commentaire}
-    members = IRC.Membership.expanded_members_or_friends(account, network, channel)
+    members = Nola.Membership.expanded_members_or_friends(account, network, channel)
     members_ids = Enum.map(members, fn({account, _, nick}) -> account.id end)
     member_names = Enum.reduce(members, %{}, fn({account, _, nick}, acc) -> Map.put(acc, account.id, nick) end)
     drinks = :ets.select(Nola.Plugins.Alcoolog.ETS, match)
              |> Enum.filter(fn({{account, _}, _vol, _cur, _cl, _deg, _name, _cmt, _meta}) -> Enum.member?(members_ids, account) end)
              |> Enum.map(fn({{account, _}, _, _, _, _, _, _, _} = object) -> {object, Map.get(member_names, account)} end)
     |> Enum.sort_by(fn({{{_, ts}, _, _, _, _, _, _, _}, _}) -> ts end, &>/2)
 
     stats = Nola.Plugins.Alcoolog.get_channel_statistics(account, network, channel)
 
     top = Enum.reduce(drinks, %{}, fn({{{account_id, _}, vol, _, _, _, _, _, _}, _}, acc) ->
       nick = Map.get(member_names, account_id)
       all = Map.get(acc, nick, 0)
       Map.put(acc, nick, all + vol)
     end)
     |> Enum.sort_by(fn({_nick, count}) -> count end, &>/2)
     # {date, single_peak}
     #
     conn
     |> assign(:title, "alcoolog")
     |> render("index.html", network: network, channel: channel, drinks: drinks, top: top, stats: stats)
   end
 
   def index_gls_json(conn = %{assigns: %{account: account}}, %{"network" => network, "chan" => channel}) do
     count = 30
     channel = NolaWeb.reformat_chan(channel)
-    members = IRC.Membership.expanded_members_or_friends(account, network, channel)
+    members = Nola.Membership.expanded_members_or_friends(account, network, channel)
     members_ids = Enum.map(members, fn({account, _, nick}) -> account.id end)
     member_names = Enum.reduce(members, %{}, fn({account, _, nick}, acc) -> Map.put(acc, account.id, nick) end)
     delay = count*((24 * 60)*60)
     now = DateTime.utc_now()
     start_date = DateTime.utc_now()
              |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
              |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
              |> DateTime.to_date()
              |> Date.to_erl()
   filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
   |> Enum.to_list
   |> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
   |> Enum.map(&Date.from_erl!(&1))
   |> Enum.map(fn(date) ->
     {date, (for {a, _, _} <- members, into: Map.new, do: {Map.get(member_names, a.id, a.id), 0})}
   end)
   |> Enum.into(Map.new)
 
     gls = Enum.reduce(members, filled, fn({account, _, _}, gls) ->
       Enum.reduce(Nola.Plugins.Alcoolog.user_over_time_gl(account, count), gls, fn({date, gl}, gls) ->
         u = Map.get(gls, date, %{})
             |> Map.put(Map.get(member_names, account.id, account.id), gl)
         Map.put(gls, date, u)
       end)
     end)
 
     dates = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
     |> Enum.to_list
     |> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
     |> Enum.map(&Date.from_erl!(&1))
 
     filled2 = Enum.map(member_names, fn({_, name}) ->
     history = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
     |> Enum.to_list
     |> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
     |> Enum.map(&Date.from_erl!(&1))
     |> Enum.map(fn(date) ->
       get_in(gls, [date, name]) #%{date: date, gl: get_in(gls, [date, name])}
     end)
       if Enum.all?(history, fn(x) -> x == 0 end) do
         nil
       else
         %{name: name, history: history}
       end
     end)
     |> Enum.filter(fn(x) -> x end)
 
       conn
       |> put_resp_content_type("application/json")
       |> text(Jason.encode!(%{labels: dates, data: filled2}))
   end
 
   def minisync(conn, %{"user_id" => user_id, "key" => key, "value" => value}) do
     account = Nola.Account.get(user_id)
     if account do
       ds = Nola.Plugins.Alcoolog.data_state()
       meta = Nola.Plugins.Alcoolog.get_user_meta(ds, account.id)
       case Float.parse(value) do
         {val, _} ->
           new_meta = Map.put(meta, String.to_existing_atom(key), val)
           Nola.Plugins.Alcoolog.put_user_meta(ds, account.id, new_meta)
         _ ->
           conn
           |> put_status(:unprocessable_entity)
           |> text("invalid value")
       end
     else
       conn
       |> put_status(:not_found)
       |> text("not found")
     end
   end
 
 end
diff --git a/lib/web/controllers/irc_controller.ex b/lib/web/controllers/irc_controller.ex
index d6114e6..441cbe7 100644
--- a/lib/web/controllers/irc_controller.ex
+++ b/lib/web/controllers/irc_controller.ex
@@ -1,101 +1,101 @@
 defmodule NolaWeb.IrcController do
   use NolaWeb, :controller
 
   plug NolaWeb.ContextPlug
 
   def index(conn, params) do
     network = Map.get(params, "network")
     channel = if c = Map.get(params, "chan"), do: NolaWeb.reformat_chan(c)
     commands = for mod <- Enum.uniq([Nola.Plugins.Account] ++ Nola.Plugins.enabled()) do
       if is_atom(mod) do
         identifier = Module.split(mod) |> List.last |> Macro.underscore
         {identifier, mod.irc_doc()}
       end
     end
     |> Enum.filter(& &1)
     |> Enum.filter(fn({_, doc}) -> doc end)
     members = cond do
       network && channel -> Enum.map(IRC.UserTrack.channel(network, channel), fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple) end)
       true ->
-        IRC.Membership.of_account(conn.assigns.account)
+        Nola.Membership.of_account(conn.assigns.account)
     end
     render conn, "index.html", network: network, commands: commands, channel: channel, members: members
   end
 
   def txt(conn, %{"name" => name}) do
     if String.contains?(name, ".txt") do
       name = String.replace(name, ".txt", "")
       data = data()
       if Map.has_key?(data, name) do
         lines = Enum.join(data[name], "\n")
         text(conn, lines)
       else
         conn
         |> put_status(404)
         |> text("Not found")
       end
     else
       do_txt(conn, name)
     end
   end
   def txt(conn, _), do: do_txt(conn, nil)
 
 
   defp do_txt(conn, nil) do
     doc = Nola.IRC.Txt.irc_doc()
     data = data()
     main = Enum.filter(data, fn({trigger, _}) -> !String.contains?(trigger, ".") end) |> Enum.into(Map.new)
     system = Enum.filter(data, fn({trigger, _}) -> String.contains?(trigger, ".") end) |> Enum.into(Map.new)
     lines = Enum.reduce(main, 0, fn({_, lines}, acc) -> acc + Enum.count(lines) end)
     conn
     |> assign(:title, "txt")
     |> render("txts.html", data: main, doc: doc, files: Enum.count(main), lines: lines, system: system)
   end
 
   defp do_txt(conn, txt) do
     data = data()
     base_url = cond do
       conn.assigns[:chan] -> "/#{conn.assigns.network}/#{NolaWeb.format_chan(conn.assigns.chan)}"
       true -> "/-"
     end
     if lines = Map.get(data, txt) do
       lines = Enum.map(lines, fn(line) ->
         line
         |> String.split("\\\\")
         |> Enum.intersperse(Phoenix.HTML.Tag.tag(:br))
       end)
       conn
       |> assign(:breadcrumbs, [{"txt", "#{base_url}/txt"}])
       |> assign(:title, "#{txt}.txt")
       |> render("txt.html", name: txt, data: lines, doc: nil)
     else
       conn
       |> put_status(404)
       |> text("Not found")
     end
   end
 
   defp data() do
     dir = Application.get_env(:nola, :data_path) <> "/irc.txt/"
     Path.wildcard(dir <> "/*.txt")
     |> Enum.reduce(%{}, fn(path, m) ->
       path = String.split(path, "/")
       file = List.last(path)
       key = String.replace(file, ".txt", "")
       data = dir <> file
       |> File.read!
       |> String.split("\n")
       |> Enum.reject(fn(line) ->
         cond do
           line == "" -> true
           !line -> true
           true -> false
         end
       end)
       Map.put(m, key, data)
     end)
     |> Enum.sort
     |> Enum.into(Map.new)
   end
 
 end
diff --git a/lib/web/controllers/page_controller.ex b/lib/web/controllers/page_controller.ex
index c5d0a57..a6b85b6 100644
--- a/lib/web/controllers/page_controller.ex
+++ b/lib/web/controllers/page_controller.ex
@@ -1,53 +1,53 @@
 defmodule NolaWeb.PageController do
   use NolaWeb, :controller
 
   plug NolaWeb.ContextPlug when action not in [:token]
   plug NolaWeb.ContextPlug, [restrict: :public] when action in [:token]
 
   def token(conn, %{"token" => token}) do
     with \
          {:ok, account, perks} <- Nola.AuthToken.lookup(token)
     do
       IO.puts("Authenticated account #{inspect account}")
       conn = put_session(conn, :account, account)
       case perks do
         nil -> redirect(conn, to: "/")
         {:redirect, path} -> redirect(conn, to: path)
         {:external_redirect, url} -> redirect(conn, external: url)
       end
     else
       z ->
         IO.inspect(z)
         text(conn, "Error: invalid or expired token")
     end
   end
 
   def index(conn = %{assigns: %{account: account}}, _) do
-    memberships = IRC.Membership.of_account(account)
+    memberships = Nola.Membership.of_account(account)
     users = IRC.UserTrack.find_by_account(account)
     metas = Nola.Account.get_all_meta(account)
     predicates = Nola.Account.get_predicates(account)
     conn
     |> assign(:title, account.name)
     |> render("user.html", users: users, memberships: memberships, metas: metas, predicates: predicates)
   end
 
   def irc(conn, _) do
     bot_helps = for mod <- Nola.IRC.env(:handlers) do
       mod.irc_doc()
     end
     render conn, "irc.html", bot_helps: bot_helps
   end
 
   def authenticate(conn, _) do
     with \
          {:account, account_id} when is_binary(account_id) <- {:account, get_session(conn, :account)},
          {:account, account} when not is_nil(account) <- {:account, Nola.Account.get(account_id)}
     do
       assign(conn, :account, account)
     else
       _ -> conn
     end
   end
 
 end
diff --git a/lib/web/live/chat_live.ex b/lib/web/live/chat_live.ex
index 0513cc2..2d5e289 100644
--- a/lib/web/live/chat_live.ex
+++ b/lib/web/live/chat_live.ex
@@ -1,120 +1,120 @@
 defmodule NolaWeb.ChatLive do
   use Phoenix.LiveView
   use Phoenix.HTML
   require Logger
 
   def mount(%{"network" => network, "chan" => chan}, %{"account" => account_id}, socket) do
     chan = NolaWeb.reformat_chan(chan)
     connection = IRC.Connection.get_network(network, chan)
     account = Nola.Account.get(account_id)
-    membership = IRC.Membership.of_account(Nola.Account.get("DRgpD4fLf8PDJMLp8Dtb"))
+    membership = Nola.Membership.of_account(Nola.Account.get("DRgpD4fLf8PDJMLp8Dtb"))
     if account && connection && Enum.member?(membership, {connection.network, chan}) do
       {:ok, _} = Registry.register(Nola.PubSub, "#{connection.network}:events", plugin: __MODULE__)
       for t <- ["messages", "triggers", "outputs", "events"] do
         {:ok, _} = Registry.register(Nola.PubSub, "#{connection.network}/#{chan}:#{t}", plugin: __MODULE__)
       end
 
       IRC.PuppetConnection.start(account, connection)
 
       users = IRC.UserTrack.channel(connection.network, chan)
       |> Enum.map(fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple) end)
       |> Enum.reduce(Map.new, fn(user = %{id: id}, acc) ->
         Map.put(acc, id, user)
       end)
 
       backlog = case Nola.IRC.Buffer.select_buffer(connection.network, chan) do
                   {backlog, _} ->
                     {backlog, _} = Enum.reduce(backlog, {backlog, nil}, &reduce_contextual_event/2)
                     Enum.reverse(backlog)
         _ -> []
       end
 
       socket = socket
       |> assign(:connection_id, connection.id)
       |> assign(:network, connection.network)
       |> assign(:chan, chan)
       |> assign(:title, "live")
       |> assign(:channel, chan)
       |> assign(:account_id, account.id)
       |> assign(:backlog, backlog)
       |> assign(:users, users)
       |> assign(:counter, 0)
 
       {:ok, socket}
     else
       {:ok, redirect(socket, to: "/")}
     end
   end
 
   def handle_event("send", %{"message" => %{"text" => text}}, socket) do
     account = Nola.Account.get(socket.assigns.account_id)
     IRC.send_message_as(account, socket.assigns.network, socket.assigns.channel, text, true)
     {:noreply, assign(socket, :counter, socket.assigns.counter + 1)}
   end
 
   def handle_info({:irc, :event, event = %{type: :join, user_id: id}}, socket) do
     if user = IRC.UserTrack.lookup(id) do
       socket = socket
       |> assign(:users, Map.put(socket.assigns.users, id, user))
       |> append_to_backlog(event)
       {:noreply, socket}
     else
       {:noreply, socket}
     end
   end
 
   def handle_info({:irc, :event, event = %{type: :nick, user_id: id, nick: nick}}, socket) do
     socket = socket
     |> assign(:users, update_in(socket.assigns.users, [id, :nick], nick))
     |> append_to_backlog(event)
     {:noreply, socket}
   end
 
   def handle_info({:irc, :event, event = %{type: :quit, user_id: id}}, socket) do
     socket = socket
     |> assign(:users, Map.delete(socket.assigns.users, id))
     |> append_to_backlog(event)
     {:noreply, socket}
   end
 
   def handle_info({:irc, :event, event = %{type: :part, user_id: id}}, socket) do
     socket = socket
     |> assign(:users, Map.delete(socket.assigns.users, id))
     |> append_to_backlog(event)
     {:noreply, socket}
   end
 
   def handle_info({:irc, :trigger, _, message}, socket) do
     handle_info({:irc, nil, message}, socket)
   end
 
   def handle_info({:irc, :text, message}, socket) do
     IO.inspect({:live_message, message})
     socket = socket
     |> append_to_backlog(message)
     {:noreply, socket}
   end
 
   def handle_info(info, socket) do
     Logger.debug("Unhandled info: #{inspect info}")
     {:noreply, socket}
   end
 
   defp append_to_backlog(socket, line) do
     {add, _} = reduce_contextual_event(line, {[], List.last(socket.assigns.backlog)})
     assign(socket, :backlog, socket.assigns.backlog ++ add)
   end
 
   defp reduce_contextual_event(line, {acc, nil}) do
     {[line | acc], line}
   end
   defp reduce_contextual_event(line, {acc, last}) do
     if NaiveDateTime.to_date(last.at) != NaiveDateTime.to_date(line.at) do
       {[%{type: :day_changed, date: NaiveDateTime.to_date(line.at), at: nil}, line | acc], line}
        else
          {[line | acc], line}
       end
 
   end
 
 end