diff --git a/lib/irc/admin_handler.ex b/lib/irc/admin_handler.ex
index 282f3c2..a462789 100644
--- a/lib/irc/admin_handler.ex
+++ b/lib/irc/admin_handler.ex
@@ -1,41 +1,41 @@
 defmodule Nola.Irc.AdminHandler do
   @moduledoc """
   # admin
 
       !op
           op; requiert admin
   """
 
   def irc_doc, do: nil
 
   def start_link(client) do
     GenServer.start_link(__MODULE__, [client])
   end
 
   def init([client]) do
     ExIRC.Client.add_handler client, self
     :ok = IRC.register("op")
     {:ok, client}
   end
 
-  def handle_info({:irc, :trigger, "op", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}, sender: sender}}, client) do
+  def handle_info({:irc, :trigger, "op", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang}, sender: sender}}, client) do
     if IRC.admin?(sender) do
       m.replyfun.({:mode, "+o"})
     else
       m.replyfun.({:kick, "non"})
     end
     {:noreply, client}
   end
 
   def handle_info({:joined, chan, sender}, client) do
     if IRC.admin?(sender) do
       ExIRC.Client.mode(client, chan, "+o", sender.nick)
     end
     {:noreply, client}
   end
 
   def handle_info(msg, client) do
     {:noreply, client}
   end
 
 end
diff --git a/lib/irc/connection.ex b/lib/irc/connection.ex
index 9bb09ed..037b7d6 100644
--- a/lib/irc/connection.ex
+++ b/lib/irc/connection.ex
@@ -1,521 +1,521 @@
 defmodule IRC.Connection do
   require Logger
   use Ecto.Schema
 
   @moduledoc """
   # IRC Connection
 
   Provides a nicer abstraction over ExIRC's handlers.
 
   ## Start connections
 
   ```
   IRC.Connection.start_link(host: "irc.random.sh", port: 6697, nick: "pouetbot", channels: ["#dev"])
 
   ## PubSub topics
 
   * `account` -- accounts change
     * {:account_change, old_account_id, new_account_id} # Sent when account merged
     * {:accounts, [{:account, network, channel, nick, account_id}] # Sent on bot join
     * {:account, network, nick, account_id} # Sent on user join
   * `message` -- aill messages (without triggers)
   * `message:private` -- all messages without a channel
   * `message:#CHANNEL` -- all messages within `#CHANNEL`
   * `triggers` -- all triggers
   * `trigger:TRIGGER` -- any message with a trigger `TRIGGER`
 
-  ## Replying to %IRC.Message{}
+  ## Replying to %Nola.Message{}
 
-  Each `IRC.Message` comes with a dedicated `replyfun`, to which you only have to pass either:
+  Each `Nola.Message` comes with a dedicated `replyfun`, to which you only have to pass either:
 
   """
   def irc_doc, do: nil
 
   @min_backoff :timer.seconds(5)
   @max_backoff :timer.seconds(2*60)
 
   embedded_schema do
     field :network, :string
     field :host, :string
     field :port, :integer
     field :nick, :string
     field :user, :string
     field :name, :string
     field :pass, :string
     field :tls, :boolean, default: false
     field :channels, {:array, :string}, default: []
   end
 
   defmodule Supervisor do
     use DynamicSupervisor
 
     def start_link() do
       DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
     end
 
     def start_child(%IRC.Connection{} = conn) do
       spec = %{id: conn.id, start: {IRC.Connection, :start_link, [conn]}, 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 changeset(params) do
     import Ecto.Changeset
 
     %__MODULE__{id: EntropyString.large_id()}
     |> cast(params, [:network, :host, :port, :nick, :user, :name, :pass, :channels, :tls])
     |> validate_required([:host, :port, :nick, :user, :name])
     |> apply_action(:insert)
   end
 
   def to_tuple(%__MODULE__{} = conn) do
     {conn.id, conn.network, conn.host, conn.port, conn.nick, conn.user, conn.name, conn.pass, conn.tls, conn.channels, nil}
   end
 
   def from_tuple({id, network, host, port, nick, user, name, pass, tls, channels, _}) do
     %__MODULE__{id: id, network: network, host: host, port: port, nick: nick, user: user, name: name, pass: pass, tls: tls, channels: channels}
   end
 
   ## -- MANAGER API
 
   def setup() do
     :dets.open_file(dets(), [])
   end
 
   def dets(), do: to_charlist(Nola.data_path("/connections.dets"))
 
   def lookup(id) do
     case :dets.lookup(dets(), id) do
       [object | _] -> from_tuple(object)
       _ -> nil
     end
   end
 
   def connections() do
     :dets.foldl(fn(object, acc) -> [from_tuple(object) | acc] end, [], dets())
   end
 
   def start_all() do
     for conn <- connections(), do: {conn, IRC.Connection.Supervisor.start_child(conn)}
   end
 
   def get_network(network, channel \\ nil) do
     spec = [{{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
       [{:==, :"$1", {:const, network}}], [:"$_"]}]
     results = Enum.map(:dets.select(dets(), spec), fn(object) -> from_tuple(object) end)
     if channel do
       Enum.find(results, fn(conn) -> Enum.member?(conn.channels, channel) end)
     else
       List.first(results)
     end
   end
 
   def get_host_nick(host, port, nick) do
     spec = [{{:_, :_, :"$1", :"$2", :"$3", :_, :_, :_, :_, :_, :_},
       [{:andalso,
         {:andalso, {:==, :"$1", {:const, host}}, {:==, :"$2", {:const, port}}},
         {:==, :"$3", {:const, nick}}}],
       [:"$_"]}
     ]
     case :dets.select(dets(), spec) do
       [object] -> from_tuple(object)
       [] -> nil
     end
   end
 
   def delete_connection(%__MODULE__{id: id} = conn) do
     :dets.delete(dets(), id)
     stop_connection(conn)
     :ok
   end
 
   def start_connection(%__MODULE__{} = conn) do
     IRC.Connection.Supervisor.start_child(conn)
   end
 
   def stop_connection(%__MODULE__{id: id}) do
     case :global.whereis_name(id) do
       pid when is_pid(pid) ->
         GenServer.stop(pid, :normal)
       _ -> :error
     end
   end
 
   def add_connection(opts) do
     case changeset(opts) do
       {:ok, conn} ->
         if existing = get_host_nick(conn.host, conn.port, conn.nick) do
           {:error, {:existing, conn}}
         else
           :dets.insert(dets(), to_tuple(conn))
           IRC.Connection.Supervisor.start_child(conn)
         end
       error -> error
     end
   end
 
   def update_connection(connection) do
     :dets.insert(dets(), to_tuple(connection))
   end
 
   def start_link(conn) do
     GenServer.start_link(__MODULE__, [conn], name: {:global, conn.id})
   end
 
   def broadcast_message(net, chan, message) do
     dispatch("conn", {:broadcast, net, chan, message}, IRC.ConnectionPubSub)
   end
   def broadcast_message(list, message) when is_list(list) do
     for {net, chan} <- list do
       broadcast_message(net, chan, message)
     end
   end
 
   def privmsg(channel, line) do
     GenServer.cast(__MODULE__, {:privmsg, channel, line})
   end
 
   def init([conn]) do
     Logger.metadata(conn: conn.id)
     backoff = :backoff.init(@min_backoff, @max_backoff)
               |> :backoff.type(:jitter)
     {:ok, %{client: nil, backoff: backoff, conn: conn, connected_server: nil, connected_port: nil, network: conn.network}, {:continue, :connect}}
   end
 
   @triggers %{
     "!" => :bang,
     "+" => :plus,
     "-" => :minus,
     "?" => :query,
     "." => :dot,
     "~" => :tilde,
     "@" => :at,
     "++" => :plus_plus,
     "--" => :minus_minus,
     "!!" => :bang_bang,
     "??" => :query_query,
     ".." => :dot_dot,
     "~~" => :tilde_tilde,
     "@@" => :at_at
   }
 
   def handle_continue(:connect, state) do
     client_opts = []
                   |> Keyword.put(:network, state.conn.network)
     {:ok, _} = Registry.register(IRC.ConnectionPubSub, "conn", [])
     client = if state.client && Process.alive?(state.client) do
       Logger.info("Reconnecting client")
       state.client
     else
       Logger.info("Connecting")
       {:ok, client} = ExIRC.Client.start_link(debug: false)
       ExIRC.Client.add_handler(client, self())
       client
     end
 
     opts = [{:nodelay, true}]
     conn_fun = if state.conn.tls, do: :connect_ssl!, else: :connect!
     apply(ExIRC.Client, conn_fun, [client, to_charlist(state.conn.host), state.conn.port, opts])
 
     {:noreply, %{state | client: client}}
   end
 
   def handle_info(:disconnected, state) do
     {delay, backoff} = :backoff.fail(state.backoff)
     Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect delay}ms")
     Process.send_after(self(), :connect, delay)
     {:noreply, %{state | backoff: backoff}}
   end
 
   def handle_info(:connect, state) do
     {:noreply, state, {:continue, :connect}}
   end
 
   def handle_cast({:privmsg, channel, line}, state) do
     irc_reply(state, {channel, nil}, line)
     {:noreply, state}
   end
 
   # Connection successful
   def handle_info({:connected, server, port}, state) do
     Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect state}")
     {_, backoff} = :backoff.succeed(state.backoff)
     ExIRC.Client.logon(state.client, state.conn.pass || "", state.conn.nick, state.conn.user, state.conn.name)
     {:noreply, %{state | backoff: backoff, connected_server: server, connected_port: port}}
   end
 
   # Logon successful
   def handle_info(:logged_in, state) do
     Logger.info("#{inspect(self())} Logged in")
     {_, backoff} = :backoff.succeed(state.backoff)
     Enum.map(state.conn.channels, &ExIRC.Client.join(state.client, &1))
     {:noreply, %{state | backoff: backoff}}
   end
 
   # ISUP
   def handle_info({:isup, network}, state) when is_binary(network) do
     Nola.UserTrack.clear_network(state.network)
     if network != state.network do
       Logger.warn("Possibly misconfigured network: #{network} != #{state.network}")
     end
     {:noreply, state}
   end
 
   # Been kicked
   def handle_info({:kicked, _sender, chan, _reason}, state) do
     ExIRC.Client.join(state.client, chan)
     {:noreply, state}
   end
 
   # Received something in a channel
   def handle_info({:received, text, sender, chan}, state) do
     user = if user = Nola.UserTrack.find_by_nick(state.network, sender.nick) do
       user
     else
       Logger.error("Could not lookup user for message: #{inspect {state.network, chan, sender.nick}}")
       user = Nola.UserTrack.joined(chan, sender, [])
       ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user..
       user
     end
     if !user do
       ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user..
       Logger.error("Could not lookup user nor create it for message: #{inspect {state.network, chan, sender.nick}}")
     else
       if !Map.get(user.options, :puppet) do
         reply_fun = fn(text) -> irc_reply(state, {chan, sender}, text) end
         account = Nola.Account.lookup(sender)
-        message = %IRC.Message{id: FlakeId.get(), transport: :irc, at: NaiveDateTime.utc_now(), text: text, network: state.network,
+        message = %Nola.Message{id: FlakeId.get(), transport: :irc, at: NaiveDateTime.utc_now(), text: text, network: state.network,
                                account: account, sender: sender, channel: chan, replyfun: reply_fun,
                                trigger: extract_trigger(text)}
         message = case Nola.UserTrack.messaged(message) do
           :ok -> message
           {:ok, message} -> message
         end
         publish(message, ["#{message.network}/#{chan}:messages"])
       end
     end
     {:noreply, state}
   end
 
   # Received a private message
   def handle_info({:received, text, sender}, state) do
     reply_fun = fn(text) -> irc_reply(state, {sender.nick, sender}, text) end
     account = Nola.Account.lookup(sender)
-    message = %IRC.Message{id: FlakeId.get(), transport: :irc, text: text, network: state.network, at: NaiveDateTime.utc_now(),
+    message = %Nola.Message{id: FlakeId.get(), transport: :irc, text: text, network: state.network, at: NaiveDateTime.utc_now(),
                            account: account, sender: sender, replyfun: reply_fun, trigger: extract_trigger(text)}
     message = case Nola.UserTrack.messaged(message) do
       :ok -> message
       {:ok, message} -> message
     end
     publish(message, ["messages:private", "#{message.network}/#{account.id}:messages"])
     {:noreply, state}
   end
 
   ## -- Broadcast
   def handle_info({:broadcast, net, account = %Nola.Account{}, message}, state) do
     if net == state.conn.network do
       user = Nola.UserTrack.find_by_account(net, account)
       if user do
         irc_reply(state, {user.nick, nil}, message)
       end
     end
     {:noreply, state}
   end
   def handle_info({:broadcast, net, chan, message}, state) do
     if net == state.conn.network && Enum.member?(state.conn.channels, chan) do
       irc_reply(state, {chan, nil}, message)
     end
     {:noreply, state}
   end
 
   ## -- UserTrack
 
   def handle_info({:joined, channel}, state) do
     ExIRC.Client.who(state.client, channel)
     {:noreply, state}
   end
 
   def handle_info({:who, channel, whos}, state) do
     accounts = Enum.map(whos, fn(who = %ExIRC.Who{nick: nick, operator?: operator}) ->
       priv = if operator, do: [:operator], else: []
       # Don't touch -- on WHO the bot joined, not the users.
       Nola.UserTrack.joined(channel, who, priv, false)
       account = Nola.Account.lookup(who)
       if account do
         {:account, who.network, channel, who.nick, account.id}
       end
     end)
     |> Enum.filter(fn(x) -> x end)
     dispatch("account", {:accounts, accounts})
     {:noreply, state}
   end
 
   def handle_info({:quit, reason, sender}, state) do
     Nola.UserTrack.quitted(sender, reason)
     {:noreply, state}
   end
 
   def handle_info({:joined, channel, sender}, state) do
     Nola.UserTrack.joined(channel, sender, [])
     account = Nola.Account.lookup(sender)
     if account do
       dispatch("account", {:account, sender.network, channel, sender.nick, account.id})
     end
     {:noreply, state}
   end
 
   def handle_info({:kicked, nick, _by, channel, _reason}, state) do
     Nola.UserTrack.parted(state.network, channel, nick)
     {:noreply, state}
   end
 
   def handle_info({:parted, channel, %ExIRC.SenderInfo{nick: nick}}, state) do
     Nola.UserTrack.parted(state.network, channel, nick)
     {:noreply, state}
   end
 
   def handle_info({:mode, [channel, mode, nick]}, state) do
     track_mode(state.network, channel, nick, mode)
     {:noreply, state}
   end
 
   def handle_info({:nick_changed, old_nick, new_nick}, state) do
     Nola.UserTrack.renamed(state.network, old_nick, new_nick)
     {:noreply, state}
   end
 
   def handle_info(unhandled, client) do
     Logger.debug("unhandled: #{inspect unhandled}")
     {:noreply, client}
   end
 
   def publish(pub), do: publish(pub, [])
 
-  def publish(m = %IRC.Message{trigger: nil}, keys) do
+  def publish(m = %Nola.Message{trigger: nil}, keys) do
     dispatch(["messages"] ++ keys, {:irc, :text, m})
   end
 
-  def publish(m = %IRC.Message{trigger: t = %IRC.Trigger{trigger: trigger}}, keys) do
+  def publish(m = %Nola.Message{trigger: t = %Nola.Trigger{trigger: trigger}}, keys) do
     dispatch(["triggers", "#{m.network}/#{m.channel}:triggers", "trigger:"<>trigger], {:irc, :trigger, trigger, m})
   end
 
   def publish_event(net, event = %{type: _}) when is_binary(net) do
     event = event
     |> Map.put(:at, NaiveDateTime.utc_now())
     |> Map.put(:network, net)
     dispatch("#{net}:events", {:irc, :event, event})
   end
   def publish_event({net, chan}, event = %{type: type}) do
     event = event
     |> Map.put(:at, NaiveDateTime.utc_now())
     |> Map.put(:network, net)
     |> Map.put(:channel, chan)
     dispatch("#{net}/#{chan}:events", {:irc, :event, event})
   end
 
   def dispatch(keys, content, sub \\ Nola.PubSub)
 
   def dispatch(key, content, sub) when is_binary(key), do: dispatch([key], content, sub)
   def dispatch(keys, content, sub) when is_list(keys) do
     Logger.debug("dispatch #{inspect keys} = #{inspect content}")
     for key <- keys do
       spawn(fn() -> Registry.dispatch(sub, key, fn h ->
         for {pid, _} <- h, do: send(pid, content)
       end) end)
     end
   end
 
   #
   # Triggers
   #
 
   def triggers, do: @triggers
 
   for {trigger, name} <- @triggers do
     def extract_trigger(unquote(trigger)<>text) do
       text = String.strip(text)
       [trigger | args] = String.split(text, " ")
-      %IRC.Trigger{type: unquote(name), trigger: String.downcase(trigger), args: args}
+      %Nola.Trigger{type: unquote(name), trigger: String.downcase(trigger), args: args}
     end
   end
 
   def extract_trigger(_), do: nil
 
   #
   # IRC Replies
   #
 
   # irc_reply(ExIRC.Client pid, {channel or nick, ExIRC.Sender}, binary | replies
   # replies :: {:kick, reason} | {:kick, nick, reason} | {:mode, mode, nick}
   defp irc_reply(state = %{client: client, network: network}, {target, _}, text) when is_binary(text) or is_list(text) do
     lines = IRC.splitlong(text)
             |> Enum.map(fn(x) -> if(is_list(x), do: x, else: String.split(x, "\n")) end)
             |> List.flatten()
     outputs = for line <- lines do
       ExIRC.Client.msg(client, :privmsg, target, line)
-      {:irc, :out, %IRC.Message{id: FlakeId.get(), transport: :irc, network: network,
+      {:irc, :out, %Nola.Message{id: FlakeId.get(), transport: :irc, network: network,
               channel: target, text: line, sender: %ExIRC.SenderInfo{nick: state.conn.nick}, at: NaiveDateTime.utc_now(), meta: %{self: true}}}
     end
     for f <- outputs, do: dispatch(["irc:outputs", "#{network}/#{target}:outputs"], f)
   end
 
   defp irc_reply(%{client: client}, {target, %{nick: nick}}, {:kick, reason}) do
     ExIRC.Client.kick(client, target, nick, reason)
   end
 
   defp irc_reply(%{client: client}, {target, _}, {:kick, nick, reason}) do
     ExIRC.Client.kick(client, target, nick, reason)
   end
 
   defp irc_reply(%{client: client}, {target, %{nick: nick}}, {:mode, mode}) do
     ExIRC.Client.mode(%{client: client}, target, mode, nick)
   end
 
   defp irc_reply(%{client: client}, target, {:mode, mode, nick}) do
     ExIRC.Client.mode(client, target, mode, nick)
   end
 
   defp irc_reply(%{client: client}, target, {:channel_mode, mode}) do
     ExIRC.Client.mode(client, target, mode)
   end
 
   defp track_mode(network, channel, nick, "+o") do
     Nola.UserTrack.change_privileges(network, channel, nick, {[:operator], []})
     :ok
   end
 
   defp track_mode(network, channel, nick, "-o") do
     Nola.UserTrack.change_privileges(network, channel, nick, {[], [:operator]})
     :ok
   end
 
   defp track_mode(network, channel, nick, "+v") do
     Nola.UserTrack.change_privileges(network, channel, nick, {[:voice], []})
     :ok
   end
 
   defp track_mode(network, channel, nick, "-v") do
     Nola.UserTrack.change_privileges(network, channel, nick, {[], [:voice]})
     :ok
   end
 
   defp track_mode(network, channel, nick, mode) do
     Logger.warn("Unhandled track_mode: #{inspect {nick, mode}}")
     :ok
   end
 
   defp server(%{conn: %{host: host, port: port}}) do
     host <> ":" <> to_string(port)
   end
 
 end
diff --git a/lib/irc/irc.ex b/lib/irc/irc.ex
index 93525e4..a1d97a2 100644
--- a/lib/irc/irc.ex
+++ b/lib/irc/irc.ex
@@ -1,79 +1,59 @@
 defmodule IRC do
 
-  defmodule Message do
-    @derive {Poison.Encoder, except: [:replyfun]}
-    defstruct [:id,
-      :text,
-      {:transport, :irc},
-      :network,
-               :account,
-               :sender,
-               :channel,
-               :trigger,
-               :replyfun,
-               :at,
-               {:meta, %{}}
-              ]
-  end
-  defmodule Trigger do
-    @derive Poison.Encoder
-    defstruct [:type, :trigger, :args]
-  end
-
   def send_message_as(account, network, channel, text, force_puppet \\ false) do
     connection = IRC.Connection.get_network(network)
     if connection && (force_puppet || IRC.PuppetConnection.whereis(account, connection)) do
       IRC.PuppetConnection.start_and_send_message(account, connection, channel, text)
     else
       user = Nola.UserTrack.find_by_account(network, account)
       nick = if(user, do: user.nick, else: account.name)
       IRC.Connection.broadcast_message(network, channel, "<#{nick}> #{text}")
     end
   end
 
   def register(key) do
     case Registry.register(Nola.PubSub, key, []) do
       {:ok, _} -> :ok
       error -> error
     end
   end
 
   def admin?(%Message{sender: sender}), do: admin?(sender)
 
   def admin?(%{nick: nick, user: user, host: host}) do
     for {n, u, h} <- Nola.IRC.env(:admins, []) do
       admin_part_match?(n, nick) && admin_part_match?(u, user) && admin_part_match?(h, host)
     end
     |> Enum.any?
   end
 
   defp admin_part_match?(:_, _), do: true
   defp admin_part_match?(a, a), do: true
   defp admin_part_match?(_, _), do: false
 
   @max_chars 440
 
   def splitlong(string, max_chars \\ 440)
 
   def splitlong(string, max_chars) when is_list(string) do
     Enum.map(string, fn(s) -> splitlong(s, max_chars) end)
     |> List.flatten()
   end
 
   def splitlong(string, max_chars) do
     string
     |> String.codepoints
     |> Enum.chunk_every(max_chars)
     |> Enum.map(&Enum.join/1)
   end
 
   def splitlong_with_prefix(string, prefix, max_chars \\ 440) do
     prefix = "#{prefix} "
     max_chars = max_chars - (length(String.codepoints(prefix)))
     string
     |> String.codepoints
     |> Enum.chunk_every(max_chars)
     |> Enum.map(fn(line) -> prefix <> Enum.join(line) end)
   end
 
 end
diff --git a/lib/irc/puppet_connection.ex b/lib/irc/puppet_connection.ex
index 75a06f3..2604876 100644
--- a/lib/irc/puppet_connection.ex
+++ b/lib/irc/puppet_connection.ex
@@ -1,238 +1,238 @@
 defmodule IRC.PuppetConnection do
   require Logger
   @min_backoff :timer.seconds(5)
   @max_backoff :timer.seconds(2*60)
   @max_idle :timer.hours(12)
   @env Mix.env
 
   defmodule Supervisor do
     use DynamicSupervisor
 
     def start_link() do
       DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
     end
 
     def start_child(%Nola.Account{id: account_id}, %IRC.Connection{id: connection_id}) do
       spec = %{id: {account_id, connection_id}, start: {IRC.PuppetConnection, :start_link, [account_id, connection_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 whereis(account = %Nola.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}) do
     {:global, name} = name(account_id, connection_id)
     case :global.whereis_name(name) do
       :undefined -> nil
       pid -> pid
     end
   end
 
   def send_message(account = %Nola.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}, channel, text) do
     GenServer.cast(name(account_id, connection_id), {:send_message, self(), channel, text})
   end
 
   def start_and_send_message(account = %Nola.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}, channel, text) do
     {:global, name} = name(account_id, connection_id)
     pid = whereis(account, connection)
     pid = if !pid do
         case IRC.PuppetConnection.Supervisor.start_child(account, connection) do
           {:ok, pid} -> pid
           {:error, {:already_started, pid}} -> pid
         end
     else
       pid
     end
     GenServer.cast(pid, {:send_message, self(), channel, text})
   end
 
   def start(account = %Nola.Account{}, connection = %IRC.Connection{}) do
     IRC.PuppetConnection.Supervisor.start_child(account, connection)
   end
 
   def start_link(account_id, connection_id) do
     GenServer.start_link(__MODULE__, [account_id, connection_id], name: name(account_id, connection_id))
   end
 
   def name(account_id, connection_id) do
     {:global, {PuppetConnection, account_id, connection_id}}
   end
 
   def init([account_id, connection_id]) do
     account = %Nola.Account{} = Nola.Account.get(account_id)
     connection = %IRC.Connection{} = IRC.Connection.lookup(connection_id)
     Logger.metadata(puppet_conn: account.id <> "@" <> connection.id)
     backoff = :backoff.init(@min_backoff, @max_backoff)
     |> :backoff.type(:jitter)
     idle = :erlang.send_after(@max_idle, self, :idle)
     {:ok, %{client: nil, backoff: backoff, idle: idle, connected: false, buffer: [], channels: [], connection_id: connection_id, account_id: account_id, connected_server: nil, connected_port: nil, network: connection.network}, {:continue, :connect}}
   end
 
   def handle_continue(:connect, state) do
     #ipv6 = if @env == :prod do
     #  subnet = Nola.Subnet.assign(state.account_id)
     #  Nola.Account.put_meta(Nola.Account.get(state.account_id), "subnet", subnet)
     #  ip = Pfx.host(subnet, 1)
     #  {:ok, ipv6} = :inet_parse.ipv6_address(to_charlist(ip))
     #  System.cmd("add-ip6", [ip])
     #  ipv6
     #end
 
     conn = IRC.Connection.lookup(state.connection_id)
     client_opts = []
                   |> Keyword.put(:network, conn.network)
     client = if state.client && Process.alive?(state.client) do
       Logger.info("Reconnecting client")
       state.client
     else
       Logger.info("Connecting")
       {:ok, client} = ExIRC.Client.start_link(debug: false)
       ExIRC.Client.add_handler(client, self())
       client
     end
 
     base_opts = [
       {:nodelay, true}
     ]
 
     #{ip, opts} = case {ipv6, :inet_res.resolve(to_charlist(conn.host), :in, :aaaa)} do
     #  {ipv6, {:ok, {:dns_rec, _dns_header, _query, rrs = [{:dns_rr, _, _, _, _, _, _, _, _, _} | _], _, _}}} ->
     #    ip = rrs
     #    |> Enum.map(fn({:dns_rr, _, :aaaa, :in, _, _, ipv6, _, _, _}) -> ipv6 end)
     #    |> Enum.shuffle()
     #    |> List.first()
 
     #    opts = [
     #      :inet6,
     #      {:ifaddr, ipv6}
     #    ]
     #    {ip, opts}
     #  _ ->
     {ip, opts} =     {to_charlist(conn.host), []}
     #end
 
     conn_fun = if conn.tls, do: :connect_ssl!, else: :connect!
     apply(ExIRC.Client, conn_fun, [client, ip, conn.port, base_opts ++ opts])
 
     {:noreply, %{state | client: client}}
   end
 
   def handle_continue(:connected, state) do
     state = Enum.reduce(Enum.reverse(state.buffer), state, fn(b, state) ->
       {:noreply, state} = handle_cast(b, state)
       state
     end)
     {:noreply, %{state | buffer: []}}
   end
 
   def handle_cast(cast = {:send_message, _pid, _channel, _text}, state = %{connected: false, buffer: buffer}) do
     {:noreply, %{state | buffer: [cast | buffer]}}
   end
 
   def handle_cast({:send_message, pid, channel, text}, state = %{connected: true}) do
     channels = if !Enum.member?(state.channels, channel) do
       ExIRC.Client.join(state.client, channel)
       [channel | state.channels]
     else
       state.channels
     end
     ExIRC.Client.msg(state.client, :privmsg, channel, text)
 
     meta = %{puppet: true, from: pid}
     account = Nola.Account.get(state.account_id)
     nick = make_nick(state)
     sender = %ExIRC.SenderInfo{network: state.network, nick: suffix_nick(nick), user: nick, host: "puppet."}
     reply_fun = fn(text) ->
       IRC.Connection.broadcast_message(state.network, channel, text)
     end
-    message = %IRC.Message{id: FlakeId.get(), at: NaiveDateTime.utc_now(), text: text, network: state.network, account: account, sender: sender, channel: channel, replyfun: reply_fun, trigger: IRC.Connection.extract_trigger(text), meta: meta}
+    message = %Nola.Message{id: FlakeId.get(), at: NaiveDateTime.utc_now(), text: text, network: state.network, account: account, sender: sender, channel: channel, replyfun: reply_fun, trigger: IRC.Connection.extract_trigger(text), meta: meta}
     message = case Nola.UserTrack.messaged(message) do
                 :ok -> message
                 {:ok, message} -> message
               end
     IRC.Connection.publish(message, ["#{message.network}/#{channel}:messages"])
 
     idle = if length(state.buffer) == 0 do
       :erlang.cancel_timer(state.idle)
       :erlang.send_after(@max_idle, self(), :idle)
     else
       state.idle
     end
 
     {:noreply, %{state | idle: idle, channels: channels}}
   end
 
   def handle_info(:idle, state) do
     ExIRC.Client.quit(state.client, "Puppet was idle for too long")
     ExIRC.Client.stop!(state.client)
     {:stop, :normal, state}
   end
 
   def handle_info(:disconnected, state) do
     {delay, backoff} = :backoff.fail(state.backoff)
     Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect delay}ms")
     Process.send_after(self(), :connect, delay)
     {:noreply, %{state | connected: false, backoff: backoff}}
   end
 
   def handle_info(:connect, state) do
     {:noreply, state, {:continue, :connect}}
   end
 
   # Connection successful
   def handle_info({:connected, server, port}, state) do
     Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect state}")
     {_, backoff} = :backoff.succeed(state.backoff)
     base_nick = make_nick(state)
     ExIRC.Client.logon(state.client, "", suffix_nick(base_nick), base_nick, "#{base_nick}'s puppet")
     {:noreply, %{state | backoff: backoff, connected_server: server, connected_port: port}}
   end
 
   # Logon successful
   def handle_info(:logged_in, state) do
     Logger.info("#{inspect(self())} Logged in")
     {_, backoff} = :backoff.succeed(state.backoff)
     # Create an UserTrack entry for the client so it's authenticated to the right account_id already.
     Nola.UserTrack.connected(state.network, suffix_nick(make_nick(state)), make_nick(state), "puppet.", state.account_id, %{puppet: true})
     {:noreply, %{state | backoff: backoff}}
   end
 
   # ISUP
   def handle_info({:isup, network}, state) do
     {:noreply, %{state | network: network, connected: true}, {:continue, :connected}}
   end
 
   # Been kicked
   def handle_info({:kicked, _sender, chan, _reason}, state) do
     {:noreply, %{state | channels: state.channels -- [chan]}}
   end
 
   def handle_info(_info, state) do
     {:noreply, state}
   end
 
   def make_nick(state) do
     account = Nola.Account.get(state.account_id)
     user = Nola.UserTrack.find_by_account(state.network, account)
     base_nick = if(user, do: user.nick, else: account.name)
     clean_nick = case String.split(base_nick, ":", parts: 2) do
                    ["@"<>nick, _] -> nick
                    [nick] -> nick
                  end
     clean_nick
   end
 
   if Mix.env == :dev do
     def suffix_nick(nick), do: "#{nick}[d]"
   else
     def suffix_nick(nick), do: "#{nick}[p]"
   end
 
 end
diff --git a/lib/matrix/room.ex b/lib/matrix/room.ex
index 757aad0..299d7cc 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 Nola.Memberships ?
           acc
         end
       else
         acc
       end
     end)
     |> Enum.filter(& &1)
 
     for m <- members, do: Nola.UserTrack.joined(state.id, %{network: "matrix", nick: m, user: m, host: "matrix."}, [], true)
 
     accounts = Nola.UserTrack.channel(state.network, state.channel)
     |> Enum.filter(& &1)
     |> Enum.map(fn(tuple) -> Nola.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
+  def handle_irc(message = %Nola.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)
     Nola.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
     Nola.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 = Nola.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 06ca993..31e237c 100644
--- a/lib/nola/account.ex
+++ b/lib/nola/account.ex
@@ -1,263 +1,263 @@
 defmodule Nola.Account do
   alias Nola.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)
       Nola.Membership.merge_account(old_id, new_id)
       Nola.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 = 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
+  defp do_lookup(message = %Nola.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 = Nola.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(Nola.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/nola/message.ex b/lib/nola/message.ex
new file mode 100644
index 0000000..4ceb9b9
--- /dev/null
+++ b/lib/nola/message.ex
@@ -0,0 +1,23 @@
+defmodule Message do
+  @moduledoc """
+  Well, a message!
+
+  """
+
+  @derive {Poison.Encoder, except: [:replyfun]}
+
+  defstruct [
+    :id,
+    :text,
+     {:transport, :irc},
+     :network,
+     :account,
+     :sender,
+     :channel,
+     :trigger,
+     :replyfun,
+     :at,
+     {:meta, %{}}
+   ]
+
+end
diff --git a/lib/nola/trigger.ex b/lib/nola/trigger.ex
new file mode 100644
index 0000000..b6502c3
--- /dev/null
+++ b/lib/nola/trigger.ex
@@ -0,0 +1,12 @@
+defmodule Trigger do
+  @moduledoc "A `Nola.Message` parsed command/trigger."
+
+  @derive Poison.Encoder
+
+  defstruct [
+    :type,
+    :trigger,
+    :args
+  ]
+
+end
diff --git a/lib/nola/user_track.ex b/lib/nola/user_track.ex
index 2a051f9..720fb58 100644
--- a/lib/nola/user_track.ex
+++ b/lib/nola/user_track.ex
@@ -1,329 +1,329 @@
 defmodule Nola.UserTrack do
   @moduledoc """
   User Track DB & Utilities
   """
 
   @ets Nola.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 || Nola.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: Nola.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: Nola.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
       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
+  def messaged(%Nola.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: Nola.Membership.touch(account, network, chan)
     if !m.account do
-      {:ok, %IRC.Message{m | account: account}}
+      {:ok, %Nola.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
         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
           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/plugins/account.ex b/lib/plugins/account.ex
index 96405bb..20abab7 100644
--- a/lib/plugins/account.ex
+++ b/lib/plugins/account.ex
@@ -1,188 +1,188 @@
 defmodule Nola.Plugins.Account do
     @moduledoc """
     # Account
 
     * **account** Get current account id and token
     * **auth `<account-id>` `<token>`** Authenticate and link the current nickname to an account
     * **auth** list authentications methods
     * **whoami** list currently authenticated users
     * **web** get a one-time login link to web
     * **enable-telegram** Link a Telegram account
     * **enable-sms** Link a SMS number
     * **enable-untappd** Link a Untappd account
     * **set-name** set account name
     * **setusermeta puppet-nick `<nick>`** Set puppet IRC nickname
     """
 
     def irc_doc, do: @moduledoc
     def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
     def init(_) do
       {:ok, _} = Registry.register(Nola.PubSub, "messages:private", [])
       {:ok, nil}
     end
 
-    def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "help"}}, state) do
+    def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "help"}}, state) do
       text = [
         "account: show current account and auth token",
         "auth: show authentications methods",
         "whoami: list authenticated users",
         "set-name <name>: set account name",
         "web: login to web",
         "enable-sms | disable-sms: enable/change or disable sms",
         "enable-telegram: link/change telegram",
         "enable-untappd: link untappd account",
         "getmeta: show meta datas",
         "setusermeta: set user meta",
       ]
       m.replyfun.(text)
       {:noreply, state}
     end
 
-    def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "auth"}}, state) do
+    def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "auth"}}, state) do
       spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
       predicates = :dets.select(Nola.Account.file("predicates"), spec)
       text = for {net, {key, value}} <- predicates, do: "#{net}: #{to_string(key)}: #{value}"
       m.replyfun.(text)
       {:noreply, state}
     end
 
-    def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "whoami"}}, state) do
+    def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "whoami"}}, state) do
       users = for user <- Nola.UserTrack.find_by_account(m.account) do
         chans = Enum.map(user.privileges, fn({chan, _}) -> chan end)
                 |> Enum.join(" ")
         "#{user.network} - #{user.nick}!#{user.username}@#{user.host} - #{chans}"
       end
       m.replyfun.(users)
       {:noreply, state}
     end
 
-    def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "account"}}, state) do
+    def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "account"}}, state) do
       account = Nola.Account.lookup(m.sender)
       text = ["Account Id: #{account.id}",
         "Authenticate to this account from another network: \"auth #{account.id} #{account.token}\" to the other bot!"]
       m.replyfun.(text)
       {:noreply, state}
     end
 
-    def handle_info({:irc, :text, m = %IRC.Message{sender: sender, text: "auth"<>_}}, state) do
+    def handle_info({:irc, :text, m = %Nola.Message{sender: sender, text: "auth"<>_}}, state) do
       #account = Nola.Account.lookup(m.sender)
       case String.split(m.text, " ") do
         ["auth", id, token] ->
           join_account(m, id, token)
         _ ->
           m.replyfun.("Invalid parameters")
       end
       {:noreply, state}
     end
 
-    def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "set-name "<>name}}, state) do
+    def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "set-name "<>name}}, state) do
       Nola.Account.update_account_name(account, name)
       m.replyfun.("Name changed: #{name}")
       {:noreply, state}
     end
 
-    def handle_info({:irc, :text, m = %IRC.Message{text: "disable-sms"}}, state) do
+    def handle_info({:irc, :text, m = %Nola.Message{text: "disable-sms"}}, state) do
       if Nola.Account.get_meta(m.account, "sms-number") do
         Nola.Account.delete_meta(m.account, "sms-number")
         m.replfyun.("SMS disabled.")
       else
         m.replyfun.("SMS already disabled.")
       end
       {:noreply, state}
     end
 
-    def handle_info({:irc, :text, m = %IRC.Message{text: "web"}}, state) do
+    def handle_info({:irc, :text, m = %Nola.Message{text: "web"}}, state) do
       auth_url = Untappd.auth_url()
       login_url = Nola.AuthToken.new_url(m.account.id, nil)
       m.replyfun.("-> " <> login_url)
       {:noreply, state}
     end
 
-    def handle_info({:irc, :text, m = %IRC.Message{text: "enable-sms"}}, state) do
+    def handle_info({:irc, :text, m = %Nola.Message{text: "enable-sms"}}, state) do
       code = String.downcase(EntropyString.small_id())
       Nola.Account.put_meta(m.account, "sms-validation-code", code)
       Nola.Account.put_meta(m.account, "sms-validation-target", m.network)
       number = Nola.IRC.Sms.my_number()
       text = "To enable or change your number for SMS messaging, please send:"
              <> " \"enable #{code}\" to #{number}"
       m.replyfun.(text)
       {:noreply, state}
     end
 
-    def handle_info({:irc, :text, m = %IRC.Message{text: "enable-telegram"}}, state) do
+    def handle_info({:irc, :text, m = %Nola.Message{text: "enable-telegram"}}, state) do
       code = String.downcase(EntropyString.small_id())
       Nola.Account.delete_meta(m.account, "telegram-id")
       Nola.Account.put_meta(m.account, "telegram-validation-code", code)
       Nola.Account.put_meta(m.account, "telegram-validation-target", m.network)
       text = "To enable or change your number for telegram messaging, please open #{Nola.Telegram.my_path()} and send:"
              <> " \"/enable #{code}\""
       m.replyfun.(text)
       {:noreply, state}
     end
 
-    def handle_info({:irc, :text, m = %IRC.Message{text: "enable-untappd"}}, state) do
+    def handle_info({:irc, :text, m = %Nola.Message{text: "enable-untappd"}}, state) do
       auth_url = Untappd.auth_url()
       login_url = Nola.AuthToken.new_url(m.account.id, {:external_redirect, auth_url})
       m.replyfun.(["To link your Untappd account, open this URL:", login_url])
       {:noreply, state}
     end
 
-    def handle_info({:irc, :text, m = %IRC.Message{text: "getmeta"<>_}}, state) do
+    def handle_info({:irc, :text, m = %Nola.Message{text: "getmeta"<>_}}, state) do
       result = case String.split(m.text, " ") do
         ["getmeta"] ->
           for {k, v} <- Nola.Account.get_all_meta(m.account) do
             case k do
               "u:"<>key -> "(user) #{key}: #{v}"
               key -> "#{key}: #{v}"
             end
           end
         ["getmeta", key] ->
           value = Nola.Account.get_meta(m.account, key)
           text = if value do
             "#{key}: #{value}"
           else
             "#{key} is not defined"
           end
         _ ->
           "usage: getmeta [key]"
       end
       m.replyfun.(result)
       {:noreply, state}
     end
 
-    def handle_info({:irc, :text, m = %IRC.Message{text: "setusermeta"<>_}}, state) do
+    def handle_info({:irc, :text, m = %Nola.Message{text: "setusermeta"<>_}}, state) do
       result = case String.split(m.text, " ") do
         ["setusermeta", key, value] ->
           Nola.Account.put_user_meta(m.account, key, value)
           "ok"
         _ ->
           "usage: setusermeta <key> <value>"
       end
       m.replyfun.(result)
       {:noreply, state}
     end
 
     def handle_info(_, state) do
       {:noreply, state}
     end
 
     defp join_account(m, id, token) do
       old_account = Nola.Account.lookup(m.sender)
       new_account = Nola.Account.get(id)
       if new_account && token == new_account.token do
         case Nola.Account.merge_account(old_account.id, new_account.id) do
           :ok ->
             if old_account.id == new_account.id do
               m.replyfun.("Already authenticated, but hello")
             else
               m.replyfun.("Accounts merged!")
             end
           _ -> m.replyfun.("Something failed :(")
         end
       else
         m.replyfun.("Invalid token")
       end
     end
 
   end
 
diff --git a/lib/plugins/alcoolog.ex b/lib/plugins/alcoolog.ex
index cc56c4f..41b5a4f 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
+  def handle_info({:irc, :trigger, santeau, m = %Nola.Message{trigger: %Nola.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
+  def handle_info({:irc, :trigger, "soif", m = %Nola.Message{trigger: %Nola.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
+  def handle_info({:irc, :trigger, "sobrepour", m = %Nola.Message{trigger: %Nola.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
+  def handle_info({:irc, :trigger, "alcoolog", m = %Nola.Message{trigger: %Nola.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
+  def handle_info({:irc, :trigger, "alcoolog", m = %Nola.Message{trigger: %Nola.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
+  def handle_info({:irc, :trigger, "alcool", m = %Nola.Message{trigger: %Nola.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
+  def handle_info({:irc, :trigger, "santai", m = %Nola.Message{trigger: %Nola.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
+  def handle_info({:irc, :trigger, "bis", m = %Nola.Message{trigger: %Nola.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
+  def handle_info({:irc, :trigger, "again", m = %Nola.Message{trigger: %Nola.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
+  def handle_info({:irc, :trigger, "moar", m = %Nola.Message{trigger: %Nola.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 = Nola.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
         for {net, chan} <- notify do
           user = Nola.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} <- Nola.Membership.notify_channels(m.account) do
               user = Nola.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
+  def handle_info({:irc, :trigger, "santai", m = %Nola.Message{trigger: %Nola.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
     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
     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
+  def handle_info({:irc, :trigger, "sobre", m = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :dot}}}, state) do
     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
+  def handle_info({:irc, :trigger, "sobre", m = %Nola.Message{trigger: %Nola.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 = Nola.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
+  def handle_info({:irc, :trigger, "alcoolisme", m = %Nola.Message{trigger: %Nola.Trigger{args: [], type: :dot}}}, state) do
     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
+  def handle_info({:irc, :trigger, "alcoolisme", m = %Nola.Message{trigger: %Nola.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 = 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 = Nola.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
+  def handle_info({:irc, :trigger, "alcoolisme", m = %Nola.Message{trigger: %Nola.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
+  def handle_info({:irc, :trigger, "alcoolisme", m = %Nola.Message{trigger: %Nola.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
+  def handle_info({:irc, :trigger, "santai", m = %Nola.Message{trigger: %Nola.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 = Nola.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
         for {net, chan} <- notify do
           user = Nola.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
+  def handle_info({:irc, :trigger, "alcoolisme", m = %Nola.Message{trigger: %Nola.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 = Nola.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/boursorama.ex b/lib/plugins/boursorama.ex
index 77977a5..025a250 100644
--- a/lib/plugins/boursorama.ex
+++ b/lib/plugins/boursorama.ex
@@ -1,58 +1,58 @@
 defmodule Nola.Plugins.Boursorama do
 
   def irc_doc() do
     """
     # bourses
 
     Un peu comme [finance](#finance), mais en un peu mieux, et un peu moins bien.
 
     Source: [boursorama.com](https://boursorama.com)
 
     * **!caca40** affiche l'état du cac40
     """
   end
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   @cac40_url "https://www.boursorama.com/bourse/actions/palmares/france/?france_filter%5Bmarket%5D=1rPCAC&france_filter%5Bsector%5D=&france_filter%5Bvariation%5D=50002&france_filter%5Bperiod%5D=1&france_filter%5Bfilter%5D="
 
   def init(_) do
     regopts = [plugin: __MODULE__]
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:cac40", regopts)
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:caca40", regopts)
     {:ok, nil}
   end
 
-  def handle_info({:irc, :trigger, cac, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) when cac in ["cac40", "caca40"] do
+  def handle_info({:irc, :trigger, cac, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang}}}, state) when cac in ["cac40", "caca40"] do
     case HTTPoison.get(@cac40_url, [], []) do
       {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
         html = Floki.parse(body)
         board = Floki.find(body, "div.c-tradingboard")
 
         cac40 = Floki.find(board, ".c-tradingboard__main > .c-tradingboard__infos")
         instrument = Floki.find(cac40, ".c-instrument")
         last = Floki.find(instrument, "span[data-ist-last]")
                |> Floki.text()
                |> String.replace(" ", "")
         variation = Floki.find(instrument, "span[data-ist-variation]")
                     |> Floki.text()
 
         sign = case variation do
           "-"<>_ -> "▼"
           "+" -> "▲"
           _ -> ""
         end
 
         m.replyfun.("caca40: #{sign} #{variation} #{last}")
 
       {:error, %HTTPoison.Response{status_code: code}} ->
         m.replyfun.("caca40: erreur http #{code}")
 
       _ ->
         m.replyfun.("caca40: erreur http")
     end
   end
 
 end
diff --git a/lib/plugins/calc.ex b/lib/plugins/calc.ex
index e58e1b1..2ff6cb4 100644
--- a/lib/plugins/calc.ex
+++ b/lib/plugins/calc.ex
@@ -1,37 +1,37 @@
 defmodule Nola.Plugins.Calc do
   @moduledoc """
   # calc
 
   * **!calc `<expression>`**: évalue l'expression mathématique `<expression>`.
   """
 
   def irc_doc, do: @moduledoc
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def init(_) do
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:calc", [plugin: __MODULE__])
     {:ok, nil}
   end
 
-  def handle_info({:irc, :trigger, "calc", message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: expr_list}}}, state) do
+  def handle_info({:irc, :trigger, "calc", message = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: expr_list}}}, state) do
     expr = Enum.join(expr_list, " ")
     result = try do
       case Abacus.eval(expr) do
         {:ok, result} -> result
         error -> inspect(error)
       end
     rescue
       error -> if(error[:message], do: "#{error.message}", else: "erreur")
     end
     message.replyfun.("#{message.sender.nick}: #{expr} = #{result}")
     {:noreply, state}
   end
 
   def handle_info(msg, state) do
     {:noreply, state}
   end
 
 end
diff --git a/lib/plugins/coronavirus.ex b/lib/plugins/coronavirus.ex
index 31483aa..afd8a33 100644
--- a/lib/plugins/coronavirus.ex
+++ b/lib/plugins/coronavirus.ex
@@ -1,172 +1,172 @@
 defmodule Nola.Plugins.Coronavirus do
   require Logger
   NimbleCSV.define(CovidCsv, separator: ",", escape: "\"")
   @moduledoc """
   # Corona Virus
 
   Données de [Johns Hopkins University](https://github.com/CSSEGISandData/COVID-19) et mises à jour a peu près tous les jours.
 
   * `!coronavirus [France | Country]`: :-)
   * `!coronavirus`: top 10 confirmés et non guéris
   * `!coronavirus confirmés`: top 10 confirmés
   * `!coronavirus morts`: top 10 morts
   * `!coronavirus soignés`: top 10 soignés
   """
   def irc_doc, do: @moduledoc
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def init(_) do
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:coronavirus", [plugin: __MODULE__])
     {:ok, nil, {:continue, :init}}
     :ignore
   end
 
   def handle_continue(:init, _) do
     date = Date.add(Date.utc_today(), -2)
     {data, _} = fetch_data(%{}, date)
     {data, next} = fetch_data(data)
     :timer.send_after(next, :update)
     {:noreply, %{data: data}}
   end
 
   def handle_info(:update, state) do
     {data, next} = fetch_data(state.data)
     :timer.send_after(next, :update)
     {:noreply, %{data: data}}
   end
 
-  def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :bang, args: args}}}, state) when args in [
+  def handle_info({:irc, :trigger, "coronavirus", m = %Nola.Message{trigger: %{type: :bang, args: args}}}, state) when args in [
     [], ["morts"], ["confirmés"], ["soignés"], ["malades"], ["n"], ["nmorts"], ["nsoignés"], ["nconfirmés"]] do
     {field, name} = case args do
       ["confirmés"] -> {:confirmed, "confirmés"}
       ["morts"] -> {:deaths, "morts"}
       ["soignés"] -> {:recovered, "soignés"}
       ["nmorts"] -> {:new_deaths, "nouveaux morts"}
       ["nconfirmés"] -> {:new_confirmed, "nouveaux confirmés"}
       ["n"] -> {:new_current, "nouveaux malades"}
       ["nsoignés"] -> {:new_recovered, "nouveaux soignés"}
       _ -> {:current, "malades"}
     end
     IO.puts("FIELD #{inspect field}")
     field_evol = String.to_atom("new_#{field}")
     sorted = state.data
              |> Enum.filter(fn({_, %{region: region}}) -> region == true end)
              |> Enum.map(fn({location, data}) -> {location, Map.get(data, field, 0), Map.get(data, field_evol, 0)} end)
              |> Enum.sort_by(fn({_,count,_}) -> count end, &>=/2)
              |> Enum.take(10)
              |> Enum.with_index()
              |> Enum.map(fn({{location, count, evol}, index}) ->
                 ev = if String.starts_with?(name, "nouveaux") do
                   ""
                 else
                   " (#{Util.plusminus(evol)})"
                 end
                "##{index+1}: #{location} #{count}#{ev}"
              end)
              |> Enum.intersperse(" - ")
              |> Enum.join()
     m.replyfun.("CORONAVIRUS TOP10 #{name}: " <> sorted)
     {:noreply, state}
   end
 
-  def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :bang, args: location}}}, state) do
+  def handle_info({:irc, :trigger, "coronavirus", m = %Nola.Message{trigger: %{type: :bang, args: location}}}, state) do
     location = Enum.join(location, " ") |> String.downcase()
     if data = Map.get(state.data, location) do
       m.replyfun.("coronavirus: #{location}: "
       <> "#{data.current} malades (#{Util.plusminus(data.new_current)}), "
       <> "#{data.confirmed} confirmés (#{Util.plusminus(data.new_confirmed)}), "
       <> "#{data.deaths} morts (#{Util.plusminus(data.new_deaths)}), "
       <> "#{data.recovered} soignés (#{Util.plusminus(data.new_recovered)}) (@ #{data.update})")
     end
     {:noreply, state}
   end
 
-  def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :query, args: location}}}, state) do
+  def handle_info({:irc, :trigger, "coronavirus", m = %Nola.Message{trigger: %{type: :query, args: location}}}, state) do
     m.replyfun.("https://github.com/CSSEGISandData/COVID-19")
     {:noreply, state}
   end
 
   # 1. Try to fetch data for today
   # 2. Fetch yesterday if no results
   defp fetch_data(current_data, date \\ nil) do
     now = Date.utc_today()
     url = fn(date) ->
       "https://github.com/CSSEGISandData/COVID-19/raw/master/csse_covid_19_data/csse_covid_19_daily_reports/#{date}.csv"
     end
     request_date = date || now
     Logger.debug("Coronavirus check date: #{inspect request_date}")
     {:ok, date_s} = Timex.format({request_date.year, request_date.month, request_date.day}, "%m-%d-%Y", :strftime)
     cur_url = url.(date_s)
     Logger.debug "Fetching URL #{cur_url}"
     case HTTPoison.get(cur_url, [], follow_redirect: true) do
       {:ok, %HTTPoison.Response{status_code: 200, body: csv}} ->
         # Parse CSV update data
         data = csv
         |> CovidCsv.parse_string()
         |> Enum.reduce(%{}, fn(line, acc) ->
           case line do
             # FIPS,Admin2,Province_State,Country_Region,Last_Update,Lat,Long_,Confirmed,Deaths,Recovered,Active,Combined_Key
             #0FIPS,Admin2,Province_State,Country_Region,Last_Update,Lat,Long_,Confirmed,Deaths,Recovered,Active,Combined_Key,Incidence_Rate,Case-Fatality_Ratio
             [_, _, state, region, update, _lat, _lng, confirmed, deaths, recovered, _active, _combined_key, _incidence_rate, _fatality_ratio] ->
               state = String.downcase(state)
               region = String.downcase(region)
               confirmed = String.to_integer(confirmed)
               deaths = String.to_integer(deaths)
               recovered = String.to_integer(recovered)
 
               current = (confirmed - recovered) - deaths
 
               entry = %{update: update, confirmed: confirmed, deaths: deaths, recovered: recovered, current: current, region: region}
 
               region_entry = Map.get(acc, region, %{update: nil, confirmed: 0, deaths: 0, recovered: 0, current: 0})
               region_entry = %{
                 update: region_entry.update || update,
                 confirmed: region_entry.confirmed + confirmed,
                 deaths: region_entry.deaths + deaths,
                 current: region_entry.current + current,
                 recovered: region_entry.recovered + recovered,
                 region: true
               }
 
               changes = if old = Map.get(current_data, region) do
                 %{
                   new_confirmed: region_entry.confirmed - old.confirmed,
                   new_current: region_entry.current - old.current,
                   new_deaths: region_entry.deaths - old.deaths,
                   new_recovered: region_entry.recovered - old.recovered,
                 }
               else
                 %{new_confirmed: 0, new_current: 0, new_deaths: 0, new_recovered: 0}
               end
 
               region_entry = Map.merge(region_entry, changes)
 
               acc = Map.put(acc, region, region_entry)
 
               acc = if state && state != "" do
                 Map.put(acc, state, entry)
               else
                 acc
               end
 
             other ->
               Logger.info("Coronavirus line failed: #{inspect line}")
               acc
           end
         end)
         Logger.info "Updated coronavirus database"
         {data, :timer.minutes(60)}
       {:ok, %HTTPoison.Response{status_code: 404}} ->
         Logger.debug "Corona 404 #{cur_url}"
         date = Date.add(date || now, -1)
         fetch_data(current_data, date)
       other ->
         Logger.error "Coronavirus: Update failed #{inspect other}"
         {current_data, :timer.minutes(5)}
     end
   end
 
 end
diff --git a/lib/plugins/correction.ex b/lib/plugins/correction.ex
index 067f468..b50733b 100644
--- a/lib/plugins/correction.ex
+++ b/lib/plugins/correction.ex
@@ -1,59 +1,59 @@
 defmodule Nola.Plugins.Correction do
   @moduledoc """
   # correction
 
   * `s/pattern/replace` replace `pattern` by `replace` in the last matching message
   """
 
   def irc_doc, do: @moduledoc
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def init(_) do
     {:ok, _} = Registry.register(Nola.PubSub, "messages", [plugin: __MODULE__])
     {:ok, _} = Registry.register(Nola.PubSub, "triggers", [plugin: __MODULE__])
     {:ok, %{}}
   end
 
   # Trigger fallback
-  def handle_info({:irc, :trigger, _, m = %IRC.Message{}}, state) do
+  def handle_info({:irc, :trigger, _, m = %Nola.Message{}}, state) do
     {:noreply, correction(m, state)}
   end
 
-  def handle_info({:irc, :text, m = %IRC.Message{}}, state) do
+  def handle_info({:irc, :text, m = %Nola.Message{}}, state) do
     {:noreply, correction(m, state)}
   end
 
   def correction(m, state) do
     history = Map.get(state, key(m), [])
     if String.starts_with?(m.text, "s/") do
       case String.split(m.text, "/") do
         ["s", match, replace | _] ->
           case Regex.compile(match) do
             {:ok, reg} ->
               repl = Enum.find(history, fn(m) -> Regex.match?(reg, m.text) end)
               if repl do
                 new_text = String.replace(repl.text, reg, replace)
                 m.replyfun.("correction: <#{repl.sender.nick}> #{new_text}")
               end
             _ ->
               m.replyfun.("correction: invalid regex")
           end
         _ -> m.replyfun.("correction: invalid regex format")
       end
       state
     else
       history = if length(history) > 100 do
         {_, history} = List.pop_at(history, 99)
         [m | history]
       else
         [m | history]
       end
       Map.put(state, key(m), history)
     end
   end
 
   defp key(%{network: net, channel: chan}), do: "#{net}/#{chan}"
 
 end
diff --git a/lib/plugins/gpt.ex b/lib/plugins/gpt.ex
index 1171d19..f89bec1 100644
--- a/lib/plugins/gpt.ex
+++ b/lib/plugins/gpt.ex
@@ -1,259 +1,259 @@
 defmodule Nola.Plugins.Gpt do
   require Logger
   import Nola.Plugins.TempRefHelper
 
   def irc_doc() do
     """
     # OpenAI GPT
 
     Uses OpenAI's GPT-3 API to bring natural language prompts to your IRC channel.
 
     _prompts_ are pre-defined prompts and parameters defined in the bot' CouchDB.
 
     _Runs_ (results of the inference of a _prompt_) are also stored in CouchDB and
     may be resumed.
 
     * **!gpt** list GPT prompts
     * **!gpt `[prompt]` `<prompt or args>`** run a prompt
     * **+gpt `[short ref|run id]` `<prompt or args>`** continue a prompt
     * **?gpt offensive `<content>`** is content offensive ?
     * **?gpt show `[short ref|run id]`** run information and web link
     * **?gpt `[prompt]`** prompt information and web link
     """
   end
 
   @couch_db "bot-plugin-openai-prompts"
   @couch_run_db "bot-plugin-gpt-history"
   @trigger "gpt"
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   defstruct [:temprefs]
 
   def get_result(id) do
     Couch.get(@couch_run_db, id)
   end
 
   def get_prompt(id) do
     Couch.get(@couch_db, id)
   end
 
   def init(_) do
     regopts = [plugin: __MODULE__]
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:#{@trigger}", regopts)
     {:ok, %__MODULE__{temprefs: new_temp_refs()}}
   end
 
-  def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [prompt | args]}}}, state) do
+  def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: [prompt | args]}}}, state) do
     case Couch.get(@couch_db, prompt) do
       {:ok, prompt} -> {:noreply, prompt(m, prompt, Enum.join(args, " "), state)}
       {:error, :not_found} ->
         m.replyfun.("gpt: prompt '#{prompt}' does not exists")
         {:noreply, state}
       error ->
         Logger.info("gpt: prompt load error: #{inspect error}")
         m.replyfun.("gpt: database error")
         {:noreply, state}
     end
   end
 
-  def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: []}}}, state) do
+  def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: []}}}, state) do
     case Couch.get(@couch_db, "_all_docs") do
       {:ok, %{"rows" => []}} -> m.replyfun.("gpt: no prompts available")
       {:ok, %{"rows" => prompts}} ->
         prompts = prompts |> Enum.map(fn(prompt) -> Map.get(prompt, "id") end) |> Enum.join(", ")
         m.replyfun.("gpt: prompts: #{prompts}")
       error ->
         Logger.info("gpt: prompt load error: #{inspect error}")
         m.replyfun.("gpt: database error")
     end
     {:noreply, state}
   end
 
-  def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :plus, args: [ref_or_id | args]}}}, state) do
+  def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :plus, args: [ref_or_id | args]}}}, state) do
     id = lookup_temp_ref(ref_or_id, state.temprefs, ref_or_id)
     case Couch.get(@couch_run_db, id) do
       {:ok, run} ->
         Logger.debug("+gpt run: #{inspect run}")
         {:noreply, continue_prompt(m, run, Enum.join(args, " "), state)}
       {:error, :not_found} ->
         m.replyfun.("gpt: ref or id not found or expired: #{inspect ref_or_id} (if using short ref, try using full id)")
         {:noreply, state}
       error ->
         Logger.info("+gpt: run load error: #{inspect error}")
         m.replyfun.("gpt: database error")
         {:noreply, state}
     end
   end
 
-  def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :query, args: ["offensive" | text]}}}, state) do
+  def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :query, args: ["offensive" | text]}}}, state) do
     text = Enum.join(text, " ")
     {moderate?, moderation} = moderation(text, m.account.id)
     reply = cond do
       moderate? -> "⚠️ #{Enum.join(moderation, ", ")}"
       !moderate? && moderation -> "👍"
       !moderate? -> "☠️ error"
     end
     m.replyfun.(reply)
     {:noreply, state}
   end
 
-  def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :query, args: ["show", ref_or_id]}}}, state) do
+  def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :query, args: ["show", ref_or_id]}}}, state) do
     id = lookup_temp_ref(ref_or_id, state.temprefs, ref_or_id)
     url = if m.channel do
       NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :result, m.network, NolaWeb.format_chan(m.channel), id)
     else
       NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :result, id)
     end
     m.replyfun.("→ #{url}")
     {:noreply, state}
   end
 
-  def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :query, args: [prompt]}}}, state) do
+  def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :query, args: [prompt]}}}, state) do
     url = if m.channel do
       NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :prompt, m.network, NolaWeb.format_chan(m.channel), prompt)
     else
       NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :prompt, prompt)
     end
     m.replyfun.("→ #{url}")
     {:noreply, state}
   end
 
   def handle_info(info, state) do
     Logger.debug("gpt: unhandled info: #{inspect info}")
     {:noreply, state}
   end
 
   defp continue_prompt(msg, run, content, state) do
     prompt_id = Map.get(run, "prompt_id")
     prompt_rev = Map.get(run, "prompt_rev")
 
     original_prompt = case Couch.get(@couch_db, prompt_id, rev: prompt_rev) do
       {:ok, prompt} -> prompt
       _ -> nil
     end
 
     if original_prompt do
       continue_prompt = %{"_id" => prompt_id,
              "_rev" => prompt_rev,
              "type" => Map.get(original_prompt, "type"),
              "parent_run_id" => Map.get(run, "_id"),
              "openai_params" => Map.get(run, "request") |> Map.delete("prompt")}
 
       continue_prompt = if prompt_string = Map.get(original_prompt, "continue_prompt") do
         full_text = get_in(run, ~w(request prompt)) <> "\n" <> Map.get(run, "response")
         continue_prompt
         |> Map.put("prompt", prompt_string)
         |> Map.put("prompt_format", "liquid")
         |> Map.put("prompt_liquid_variables", %{"previous" => full_text})
       else
         prompt_content_tag = if content != "", do: " {{content}}", else: ""
         string = get_in(run, ~w(request prompt)) <> "\n" <> Map.get(run, "response") <> prompt_content_tag
         continue_prompt
         |> Map.put("prompt", string)
         |> Map.put("prompt_format", "liquid")
       end
 
       prompt(msg, continue_prompt, content, state)
     else
       msg.replyfun.("gpt: cannot continue this prompt: original prompt not found #{prompt_id}@v#{prompt_rev}")
       state
     end
   end
 
   defp prompt(msg, prompt = %{"type" => "completions", "prompt" => prompt_template}, content, state) do
     Logger.debug("gpt:prompt/4 #{inspect prompt}")
     prompt_text = case Map.get(prompt, "prompt_format", "liquid") do
       "liquid" -> Tmpl.render(prompt_template, msg, Map.merge(Map.get(prompt, "prompt_liquid_variables", %{}), %{"content" => content}))
       "norender" -> prompt_template
     end
 
     args = Map.get(prompt, "openai_params")
     |> Map.put("prompt", prompt_text)
     |> Map.put("user", msg.account.id)
 
     {moderate?, moderation} = moderation(content, msg.account.id)
     if moderate?, do: msg.replyfun.("⚠️ offensive input: #{Enum.join(moderation, ", ")}")
 
     Logger.debug("GPT: request #{inspect args}")
     case OpenAi.post("/v1/completions", args) do
       {:ok, %{"choices" => [%{"text" => text, "finish_reason" => finish_reason} | _], "usage" => usage, "id" => gpt_id, "created" => created}} ->
         text = String.trim(text)
         {o_moderate?, o_moderation} = moderation(text, msg.account.id)
         if o_moderate?, do: msg.replyfun.("🚨 offensive output: #{Enum.join(o_moderation, ", ")}")
         msg.replyfun.(text)
         doc = %{"id" => FlakeId.get(),
                 "prompt_id" => Map.get(prompt, "_id"),
                 "prompt_rev" => Map.get(prompt, "_rev"),
                 "network" => msg.network,
                 "channel" => msg.channel,
                 "nick" => msg.sender.nick,
                 "account_id" => (if msg.account, do: msg.account.id),
                 "request" => args,
                 "response" => text,
                 "message_at" => msg.at,
                 "reply_at" => DateTime.utc_now(),
                 "gpt_id" => gpt_id,
                 "gpt_at" => created,
                 "gpt_usage" => usage,
                 "type" => "completions",
                 "parent_run_id" => Map.get(prompt, "parent_run_id"),
                 "moderation" => %{"input" => %{flagged: moderate?, categories: moderation},
                                   "output" => %{flagged: o_moderate?, categories: o_moderation}
                 }
         }
         Logger.debug("Saving result to couch: #{inspect doc}")
         {id, ref, temprefs} = case Couch.post(@couch_run_db, doc) do
           {:ok, id, _rev} ->
             {ref, temprefs} = put_temp_ref(id, state.temprefs)
             {id, ref, temprefs}
           error ->
             Logger.error("Failed to save to Couch: #{inspect error}")
             {nil, nil, state.temprefs}
         end
         stop = cond do
           finish_reason == "stop" -> ""
           finish_reason == "length" -> " — truncated"
           true -> " — #{finish_reason}"
         end
         ref_and_prefix = if Map.get(usage, "completion_tokens", 0) == 0 do
           "GPT had nothing else to say :( ↪ #{ref || "✗"}"
         else
           "   ↪ #{ref || "✗"}"
         end
         msg.replyfun.(ref_and_prefix <>
            stop <>
            " — #{Map.get(usage, "total_tokens", 0)}" <>
            " (#{Map.get(usage, "prompt_tokens", 0)}/#{Map.get(usage, "completion_tokens", 0)}) tokens" <>
            " — #{id || "save failed"}")
         %__MODULE__{state | temprefs: temprefs}
       {:error, atom} when is_atom(atom) ->
         Logger.error("gpt error: #{inspect atom}")
         msg.replyfun.("gpt: ☠️  #{to_string(atom)}") 
         state
       error ->
         Logger.error("gpt error: #{inspect error}")
         msg.replyfun.("gpt: ☠️ ")
         state
     end
   end
 
   defp moderation(content, user_id) do
     case OpenAi.post("/v1/moderations", %{"input" => content, "user" => user_id}) do
       {:ok, %{"results" => [%{"flagged" => true, "categories" => categories} | _]}} ->
         cat = categories
         |> Enum.filter(fn({_key, value}) -> value end)
         |> Enum.map(fn({key, _}) -> key end)
         {true, cat}
       {:ok, moderation} ->
         Logger.debug("gpt: moderation: not flagged, #{inspect moderation}")
         {false, true}
       error ->
         Logger.error("gpt: moderation error: #{inspect error}")
         {false, false}
     end
   end
 
 end
diff --git a/lib/plugins/logger.ex b/lib/plugins/logger.ex
index 77bee8b..3d643d3 100644
--- a/lib/plugins/logger.ex
+++ b/lib/plugins/logger.ex
@@ -1,71 +1,71 @@
 defmodule Nola.Plugins.Logger do
   require Logger
 
   @couch_db "bot-logs"
 
   def irc_doc(), do: nil
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def init([]) do
     regopts = [plugin: __MODULE__]
     {:ok, _} = Registry.register(Nola.PubSub, "triggers", regopts)
     {:ok, _} = Registry.register(Nola.PubSub, "messages", regopts)
     {:ok, _} = Registry.register(Nola.PubSub, "messages:telegram", regopts)
     {:ok, _} = Registry.register(Nola.PubSub, "irc:outputs", regopts)
     {:ok, _} = Registry.register(Nola.PubSub, "messages:private", regopts)
     {:ok, nil}
   end
 
   def handle_info({:irc, :trigger, _, m}, state) do
     {:noreply, log(m, state)}
   end
 
   def handle_info({:irc, :text, m}, state) do
     {:noreply, log(m, state)}
   end
 
   def handle_info(info, state) do
     Logger.debug("logger_plugin: unhandled info: #{info}")
     {:noreply, state}
   end
 
   def log(entry, state) do
     case Couch.post(@couch_db, format_to_db(entry)) do
       {:ok, id, _rev} ->
         Logger.debug("logger_plugin: saved: #{inspect id}")
         state
       error ->
         Logger.error("logger_plugin: save failed: #{inspect error}")
     end
   rescue
     e ->
       Logger.error("logger_plugin: rescued processing for #{inspect entry}: #{inspect e}")
       Logger.error(Exception.format(:error, e, __STACKTRACE__))
       state
   catch
     e, b ->
       Logger.error("logger_plugin: catched processing for #{inspect entry}: #{inspect e}")
       Logger.error(Exception.format(e, b, __STACKTRACE__))
       state
   end
 
-  def format_to_db(msg = %IRC.Message{id: id}) do
+  def format_to_db(msg = %Nola.Message{id: id}) do
     msg
     |> Poison.encode!()
     |> Map.drop("id")
 
     %{"_id" => id || FlakeId.get(),
       "type" => "irc.message/v1",
       "object" => msg}
   end
 
   def format_to_db(anything) do
     %{"_id" => FlakeId.get(),
       "type" => "object",
        "object" => anything}
   end
 
 end
diff --git a/lib/plugins/outline.ex b/lib/plugins/outline.ex
index 1f1c1e1..ba8314d 100644
--- a/lib/plugins/outline.ex
+++ b/lib/plugins/outline.ex
@@ -1,108 +1,108 @@
 defmodule Nola.Plugins.Outline do
   @moduledoc """
   # outline auto-link
 
   Envoie un lien vers Outline quand un lien est envoyé.
 
   * **!outline `<url>`** crée un lien outline pour `<url>`.
   * **+outline `<host>`** active outline pour `<host>`.
   * **-outline `<host>`** désactive outline pour `<host>`.
   """
   def short_irc_doc, do: false
   def irc_doc, do: @moduledoc
   require Logger
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   defstruct [:file, :hosts]
 
   def init([]) do
     regopts = [plugin: __MODULE__]
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:outline", regopts)
     {:ok, _} = Registry.register(Nola.PubSub, "messages", regopts)
     file = Path.join(Nola.data_path, "/outline.txt")
     hosts = case File.read(file) do
       {:error, :enoent} ->
         []
       {:ok, lines} ->
         String.split(lines, "\n", trim: true)
     end
     {:ok, %__MODULE__{file: file, hosts: hosts}}
   end
 
-  def handle_info({:irc, :trigger, "outline", message = %IRC.Message{trigger: %IRC.Trigger{type: :plus, args: [host]}}}, state) do
+  def handle_info({:irc, :trigger, "outline", message = %Nola.Message{trigger: %Nola.Trigger{type: :plus, args: [host]}}}, state) do
     state = %{state | hosts: [host | state.hosts]}
     save(state)
     message.replyfun.("ok")
     {:noreply, state}
   end
 
-  def handle_info({:irc, :trigger, "outline", message = %IRC.Message{trigger: %IRC.Trigger{type: :minus, args: [host]}}}, state) do
+  def handle_info({:irc, :trigger, "outline", message = %Nola.Message{trigger: %Nola.Trigger{type: :minus, args: [host]}}}, state) do
     state = %{state | hosts: List.delete(state.hosts, host)}
     save(state)
     message.replyfun.("ok")
     {:noreply, state}
   end
 
-  def handle_info({:irc, :trigger, "outline", message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [url]}}}, state) do
+  def handle_info({:irc, :trigger, "outline", message = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: [url]}}}, state) do
     line = "-> #{outline(url)}"
     message.replyfun.(line)
   end
 
-  def handle_info({:irc, :text, message = %IRC.Message{text: text}}, state) do
+  def handle_info({:irc, :text, message = %Nola.Message{text: text}}, state) do
     String.split(text)
     |> Enum.map(fn(word) ->
       if String.starts_with?(word, "http://") || String.starts_with?(word, "https://") do
         uri = URI.parse(word)
         if uri.scheme && uri.host do
           if Enum.any?(state.hosts, fn(host) -> String.ends_with?(uri.host, host) end) do
             outline_url = outline(word)
             line = "-> #{outline_url}"
             message.replyfun.(line)
           end
        end
       end
     end)
     {:noreply, state}
   end
 
   def handle_info(msg, state) do
     {:noreply, state}
   end
 
   def save(state = %{file: file, hosts: hosts}) do
     string = Enum.join(hosts, "\n")
     File.write(file, string)
   end
 
   def outline(url) do
     unexpanded = "https://outline.com/#{url}"
     headers = [
       {"User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:77.0) Gecko/20100101 Firefox/77.0"},
       {"Accept", "*/*"},
       {"Accept-Language", "en-US,en;q=0.5"},
       {"Origin", "https://outline.com"},
       {"DNT", "1"},
       {"Referer", unexpanded},
       {"Pragma", "no-cache"},
       {"Cache-Control", "no-cache"}
     ]
     params = %{"source_url" => url}
     case HTTPoison.get("https://api.outline.com/v3/parse_article", headers, params: params) do
       {:ok, %HTTPoison.Response{status_code: 200, body: json}} ->
         body = Poison.decode!(json)
         if Map.get(body, "success") do
           code = get_in(body, ["data", "short_code"])
           "https://outline.com/#{code}"
         else
           unexpanded
         end
       error ->
         Logger.info("outline.com error: #{inspect error}")
         unexpanded
     end
   end
 
 end
diff --git a/lib/plugins/preums.ex b/lib/plugins/preums.ex
index 505ce7f..83d99cd 100644
--- a/lib/plugins/preums.ex
+++ b/lib/plugins/preums.ex
@@ -1,276 +1,276 @@
 defmodule Nola.Plugins.Preums do
   @moduledoc """
   # preums !!!
 
   * `!preums`: affiche le preums du jour
   * `.preums`: stats des preums
   """
 
   # WIP Scores
   # L'idée c'est de donner un score pour mettre un peu de challenge en pénalisant les preums faciles.
   #
   # Un preums ne vaut pas 1 point, mais plutôt 0.10 ou 0.05, et on arrondi au plus proche. C'est un jeu sur le long
   # terme. Un gros bonus pourrait apporter beaucoup de points.
   #
   # Il faudrait ces données:
   #   - moyenne des preums
   #   - activité récente du channel et par nb actifs d'utilisateurs
   #      (aggréger memberships+usertrack last_active ?)
   #      (faire des stats d'activité habituelle (un peu a la pisg) ?)
   #   - preums consécutifs
   #
   # Malus:
   #   - est proche de la moyenne en faible activité
   #   - trop consécutif de l'utilisateur sauf si activité
   #
   # Bonus:
   #   - plus le preums est éloigné de la moyenne
   #   - après 18h double
   #   - plus l'activité est élévée, exponentiel selon la moyenne
   #   - derns entre 4 et 6 (pourrait être adapté selon les stats d'activité)
   #
   # WIP Badges:
   #   - derns
   #   - streaks
   #   - faciles
   #   - ?
 
   require Logger
 
   @perfects [~r/preum(s|)/i]
 
   # dets {{chan, day = {yyyy, mm, dd}}, nick, now, perfect?, text}
 
   def all(dets) do
     :dets.foldl(fn(i, acc) -> [i|acc] end, [], dets)
   end
 
   def all(dets, channel) do
     fun = fn({{chan, date}, account_id, time, perfect, text}, acc) ->
       if channel == chan do
         [%{date: date, account_id: account_id, time: time, perfect: perfect, text: text} | acc]
       else
         acc
       end
     end
     :dets.foldl(fun, [], dets)
   end
 
   def topnicks(dets, channel, options \\ []) do
     sort_elem = case Keyword.get(options, :sort_by, :score) do
       :score -> 1
       :count -> 0
     end
 
     fun = fn(x = {{chan, date}, account_id, time, perfect, text}, acc) ->
       if (channel == nil and chan) or (channel == chan) do
         {count, points} = Map.get(acc, account_id, {0, 0})
         score = score(chan, account_id, time, perfect, text)
         Map.put(acc, account_id, {count + 1, points + score})
       else
         acc
       end
     end
     :dets.foldl(fun, %{}, dets)
     |> Enum.sort_by(fn({_account_id, value}) -> elem(value, sort_elem) end, &>=/2)
   end
 
   def irc_doc, do: @moduledoc
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def dets do
     (Nola.data_path() <> "/preums.dets") |> String.to_charlist()
   end
 
   def init([]) do
     regopts = [plugin: __MODULE__]
     {:ok, _} = Registry.register(Nola.PubSub, "account", regopts)
     {:ok, _} = Registry.register(Nola.PubSub, "messages", regopts)
     {:ok, _} = Registry.register(Nola.PubSub, "triggers", regopts)
     {:ok, dets} = :dets.open_file(dets(), [{:repair, :force}])
     Util.ets_mutate_select_each(:dets, dets, [{:"$1", [], [:"$1"]}], fn(table, obj) ->
       {key, nick, now, perfect, text} = obj
       case key do
         {{net, {bork,chan}}, date} ->
           :dets.delete(table, key)
           nick = if Nola.Account.get(nick) do
             nick
           else
             if acct = Nola.Account.find_always_by_nick(net, nil, nick) do
               acct.id
             else
               nick
             end
           end
           :dets.insert(table, { { {net,chan}, date }, nick, now, perfect, text})
         {{_net, nil}, _} ->
           :dets.delete(table, key)
         {{net, chan}, date} ->
           if !Nola.Account.get(nick) do
             if acct = Nola.Account.find_always_by_nick(net, chan, nick) do
               :dets.delete(table, key)
               :dets.insert(table, { { {net,chan}, date }, acct.id, now, perfect, text})
             end
           end
         _ ->
           Logger.debug("DID NOT FIX: #{inspect key}")
       end
     end)
     {:ok, %{dets: dets}}
   end
 
   # Latest
-  def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) do
+  def handle_info({:irc, :trigger, "preums", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang}}}, state) do
     channelkey = {m.network, m.channel}
     state = handle_preums(m, state)
     tz = timezone(channelkey)
     {:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
     date = {now.year, now.month, now.day}
     key = {channelkey, date}
     chan_cache = Map.get(state, channelkey, %{})
     item = if i = Map.get(chan_cache, date) do
       i
     else
       case :dets.lookup(state.dets, key) do
         [item = {^key, _account_id, _now, _perfect, _text}] -> item
         _ -> nil
       end
     end
 
     if item do
       {_, account_id, date, _perfect, text} = item
       h = "#{date.hour}:#{date.minute}:#{date.second}"
       account = Nola.Account.get(account_id)
       user = Nola.UserTrack.find_by_account(m.network, account)
       nick = if(user, do: user.nick, else: account.name)
       m.replyfun.("preums: #{nick} à #{h}: “#{text}”")
     end
     {:noreply, state}
   end
 
   # Stats
-  def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :dot}}}, state) do
+  def handle_info({:irc, :trigger, "preums", m = %Nola.Message{trigger: %Nola.Trigger{type: :dot}}}, state) do
     channel = {m.network, m.channel}
     state = handle_preums(m, state)
     top = topnicks(state.dets, channel, sort_by: :score)
           |> Enum.map(fn({account_id, {count, score}}) ->
             account = Nola.Account.get(account_id)
             user = Nola.UserTrack.find_by_account(m.network, account)
             nick = if(user, do: user.nick, else: account.name)
             "#{nick}: #{score} (#{count})"
           end)
           |> Enum.intersperse(", ")
           |> Enum.join("")
     msg = unless top == "" do
       "top preums: #{top}"
     else
       "vous êtes tous nuls"
     end
     m.replyfun.(msg)
     {:noreply, state}
   end
 
   # Help
-  def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :query}}}, state) do
+  def handle_info({:irc, :trigger, "preums", m = %Nola.Message{trigger: %Nola.Trigger{type: :query}}}, state) do
     state = handle_preums(m, state)
     msg = "!preums - preums du jour, .preums top preumseurs"
     m.replymsg.(msg)
     {:noreply, state}
   end
 
   # Trigger fallback
-  def handle_info({:irc, :trigger, _, m = %IRC.Message{}}, state) do
+  def handle_info({:irc, :trigger, _, m = %Nola.Message{}}, state) do
     state = handle_preums(m, state)
     {:noreply, state}
   end
 
   # Message fallback
-  def handle_info({:irc, :text, m = %IRC.Message{}}, state) do
+  def handle_info({:irc, :text, m = %Nola.Message{}}, state) do
     {:noreply, handle_preums(m, state)}
   end
 
   # Account
   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) ->
       rename_object_owner(table, obj, new_id)
     end)
     {:noreply, state}
   end
 
   # Account: move from nick to account id
   # FIXME: Doesn't seem to work.
   def handle_info({:accounts, accounts}, state) do
     for x={:account, _net, _chan, _nick, _account_id} <- accounts do
       handle_info(x, state)
     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("account:: merging #{nick} -> #{account_id}")
       rename_object_owner(table, obj, account_id)
     end)
     {:noreply, state}
   end
 
   def handle_info(_, dets) do
     {:noreply, dets}
   end
 
 
   defp rename_object_owner(table, object = {key, _, now, perfect, time}, new_id) do
     :dets.delete_object(table, key)
     :dets.insert(table, {key, new_id, now, perfect, time})
   end
 
   defp timezone(channel) do
     env = Application.get_env(:nola, Nola.Plugins.Preums, [])
     channels = Keyword.get(env, :channels, %{})
     channel_settings = Map.get(channels, channel, [])
     default = Keyword.get(env, :default_tz, "Europe/Paris")
     Keyword.get(channel_settings, :tz, default) || default
   end
 
-  defp handle_preums(%IRC.Message{channel: nil}, state) do
+  defp handle_preums(%Nola.Message{channel: nil}, state) do
     state
   end
 
-  defp handle_preums(m = %IRC.Message{text: text, sender: sender}, state) do
+  defp handle_preums(m = %Nola.Message{text: text, sender: sender}, state) do
     channel = {m.network, m.channel}
     tz = timezone(channel)
     {:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
     date = {now.year, now.month, now.day}
     key = {channel, date}
     chan_cache = Map.get(state, channel, %{})
     unless i = Map.get(chan_cache, date) do
       case :dets.lookup(state.dets, key) do
         [item = {^key, _nick, _now, _perfect, _text}] ->
           # Preums lost, but wasn't cached
           Map.put(state, channel, %{date => item})
         [] ->
           # Preums won!
           perfect? = Enum.any?(@perfects, fn(perfect) -> Regex.match?(perfect, text) end)
           item = {key, m.account.id, now, perfect?, text}
           :dets.insert(state.dets, item)
           :dets.sync(state.dets)
           Map.put(state, channel, %{date => item})
         {:error, _} = error ->
           Logger.error("#{__MODULE__} dets lookup failed: #{inspect error}")
           state
       end
     else
       state
     end
   end
 
   def score(_chan, _account, _time, _perfect, _text) do
     1
   end
 
 
 end
diff --git a/lib/plugins/quatre_cent_vingt.ex b/lib/plugins/quatre_cent_vingt.ex
index 254f5ce..6b3cc46 100644
--- a/lib/plugins/quatre_cent_vingt.ex
+++ b/lib/plugins/quatre_cent_vingt.ex
@@ -1,149 +1,149 @@
 defmodule Nola.Plugins.QuatreCentVingt do
   require Logger
 
   @moduledoc """
   # 420
 
   * **!420**: recorde un nouveau 420.
   * **!420*x**: recorde un nouveau 420*x (*2 = 840, ...) (à vous de faire la multiplication).
   * **!420 pseudo**: stats du pseudo.
   """
 
   @achievements %{
     1    => ["[le premier… il faut bien commencer un jour]"],
     10   => ["T'en es seulement à 10 ? ╭∩╮(Ο_Ο)╭∩╮"],
     42   => ["Bravo, et est-ce que autant de pétards t'on aidés à trouver la Réponse ? ٩(- ̮̮̃-̃)۶ [42]"],
     100  => ["°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ 100 °º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸"],
     115  => [" ۜ\(סּںסּَ` )/ۜ 115!!"]
   }
 
   @emojis [
     "\\o/",
     "~o~",
     "~~o∞~~",
     "*\\o/*",
     "**\\o/**",
     "*ô*",
   ]
 
   @coeffs Range.new(1, 100)
 
   def irc_doc, do: @moduledoc
 
   def start_link, do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
 
   def init(_) do
     for coeff <- @coeffs do
       {:ok, _} = Registry.register(Nola.PubSub, "trigger:#{420*coeff}", [plugin: __MODULE__])
     end
     {:ok, _} = Registry.register(Nola.PubSub, "account", [plugin: __MODULE__])
     dets_filename = (Nola.data_path() <> "/420.dets") |> String.to_charlist
     {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag},{:repair,:force}])
     {:ok, dets}
     :ignore
   end
 
   for coeff <- @coeffs do
     qvc = to_string(420 * coeff)
-    def handle_info({:irc, :trigger, unquote(qvc), m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :bang}}}, dets) do
+    def handle_info({:irc, :trigger, unquote(qvc), m = %Nola.Message{trigger: %Nola.Trigger{args: [], type: :bang}}}, dets) do
       {count, last} = get_statistics_for_nick(dets, m.account.id)
       count = count + unquote(coeff)
       text = achievement_text(count)
       now = DateTime.to_unix(DateTime.utc_now())-1 # this is ugly
       for i <- Range.new(1, unquote(coeff)) do
         :ok = :dets.insert(dets, {m.account.id, now+i})
       end
       last_s = if last do
         last_s = format_relative_timestamp(last)
         " (le dernier était #{last_s})"
       else
         ""
       end
       m.replyfun.("#{m.sender.nick} 420 +#{unquote(coeff)} #{text}#{last_s}")
       {:noreply, dets}
     end
   end
 
-  def handle_info({:irc, :trigger, "420", m = %IRC.Message{trigger: %IRC.Trigger{args: [nick], type: :bang}}}, dets) do
+  def handle_info({:irc, :trigger, "420", m = %Nola.Message{trigger: %Nola.Trigger{args: [nick], type: :bang}}}, dets) do
     account = Nola.Account.find_by_nick(m.network, nick)
     if account do
       text = case get_statistics_for_nick(dets, m.account.id) do
         {0, _} -> "#{nick} n'a jamais !420 ... honte à lui."
         {count, last} ->
           last_s = format_relative_timestamp(last)
           "#{nick} 420: total #{count}, le dernier #{last_s}"
       end
       m.replyfun.(text)
     else
       m.replyfun.("je connais pas de #{nick}")
     end
     {:noreply, dets}
   end
 
   # Account
   def handle_info({:account_change, old_id, new_id}, dets) do
     spec = [{{:"$1", :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
     Util.ets_mutate_select_each(:dets, dets, spec, fn(table, obj) ->
       rename_object_owner(table, obj, new_id)
     end)
     {:noreply, dets}
   end
 
   # Account: move from nick to account id
   def handle_info({:accounts, accounts}, dets) do
     for x={:account, _net, _chan, _nick, _account_id} <- accounts do
       handle_info(x, dets)
     end
     {:noreply, dets}
   end
   def handle_info({:account, _net, _chan, nick, account_id}, dets) do
     nick = String.downcase(nick)
     spec = [{{:"$1", :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}]
     Util.ets_mutate_select_each(:dets, dets, spec, fn(table, obj) ->
       Logger.debug("account:: merging #{nick} -> #{account_id}")
       rename_object_owner(table, obj, account_id)
     end)
     {:noreply, dets}
   end
 
   def handle_info(_, dets) do
     {:noreply, dets}
   end
 
   defp rename_object_owner(table, object = {_, at}, account_id) do
     :dets.delete_object(table, object)
     :dets.insert(table, {account_id, at})
   end
 
 
   defp format_relative_timestamp(timestamp) do
     alias Timex.Format.DateTime.Formatters
     alias Timex.Timezone
     date = timestamp
     |> DateTime.from_unix!
     |> 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 get_statistics_for_nick(dets, acct) do
     qvc = :dets.lookup(dets, acct) |> Enum.sort
     count = Enum.reduce(qvc, 0, fn(_, acc) -> acc + 1 end)
     {_, last} = List.last(qvc) || {nil, nil}
     {count, last}
   end
 
   @achievements_keys Map.keys(@achievements)
   defp achievement_text(count) when count in @achievements_keys do
     Enum.random(Map.get(@achievements, count))
   end
 
   defp achievement_text(count) do
     emoji = Enum.random(@emojis)
     "#{emoji} [#{count}]"
   end
 
 end
diff --git a/lib/plugins/radio_france.ex b/lib/plugins/radio_france.ex
index a2a1c7b..d95c54a 100644
--- a/lib/plugins/radio_france.ex
+++ b/lib/plugins/radio_france.ex
@@ -1,133 +1,133 @@
 defmodule Nola.Plugins.RadioFrance do
   require Logger
 
   def irc_doc() do
     """
     # radio france
 
     Qu'est ce qu'on écoute sur radio france ?
 
     * **!radiofrance `[station]`, !rf `[station]`**
     * **!fip, !inter, !info, !bleu, !culture, !musique, !fip `[sous-station]`, !bleu `[région]`**
     """
   end
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   @trigger "radiofrance"
   @shortcuts ~w(fip inter info bleu culture musique)
 
   def init(_) do
     regopts = [plugin: __MODULE__]
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:radiofrance", regopts)
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:rf", regopts)
     for s <- @shortcuts do
       {:ok, _} = Registry.register(Nola.PubSub, "trigger:#{s}", regopts)
     end
     {:ok, nil}
   end
 
-  def handle_info({:irc, :trigger, "rf", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) do
+  def handle_info({:irc, :trigger, "rf", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang}}}, state) do
     handle_info({:irc, :trigger, "radiofrance", m}, state)
   end
 
-  def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: []}}}, state) do
+  def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: []}}}, state) do
     m.replyfun.("radiofrance: précisez la station!")
     {:noreply, state}
   end
 
-  def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) do
+  def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}}, state) do
     now(args_to_station(args), m)
     {:noreply, state}
   end
 
-  def handle_info({:irc, :trigger, trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) when trigger in @shortcuts do
+  def handle_info({:irc, :trigger, trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}}, state) when trigger in @shortcuts do
     now(args_to_station([trigger | args]), m)
     {:noreply, state}
   end
 
   defp args_to_station(args) do
     args
     |> Enum.map(&unalias/1)
     |> Enum.map(&String.downcase/1)
     |> Enum.join("_")
   end
 
   def handle_info(info, state) do
     Logger.debug("unhandled info: #{inspect info}")
     {:noreply, state}
   end
 
   defp now(station, m) when is_binary(station) do
     case HTTPoison.get(np_url(station), [], []) do
       {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
         json = Poison.decode!(body)
         song? = !!get_in(json, ["now", "song"])
         station = reformat_station_name(get_in(json, ["now", "stationName"]))
         now_title = get_in(json, ["now", "firstLine", "title"])
         now_subtitle = get_in(json, ["now", "secondLine", "title"])        
         next_title = get_in(json, ["next", "firstLine", "title"])
         next_subtitle = get_in(json, ["next", "secondLine", "title"])
         next_song? = !!get_in(json, ["next", "song"])
         next_at = get_in(json, ["next", "startTime"])
 
         now = format_title(song?, now_title, now_subtitle)
         prefix = if song?, do: "🎶", else: "🎤"
         m.replyfun.("#{prefix} #{station}: #{now}")
         
         next = format_title(song?, next_title, next_subtitle)
         if next do
           next_prefix = if next_at do
             next_date = DateTime.from_unix!(next_at)
             in_seconds = DateTime.diff(next_date, DateTime.utc_now())
             in_minutes = ceil(in_seconds / 60)
             if in_minutes >= 5 do
               if next_song?, do: "#{in_minutes}m 🔜", else: "dans #{in_minutes} minutes:"
             else
               if next_song?, do: "🔜", else: "suivi de:"
             end
           else
             if next_song?, do: "🔜", else: "à suivre:"
           end
           m.replyfun.("#{next_prefix} #{next}")
         end
 
       {:error, %HTTPoison.Response{status_code: 404}} ->
         m.replyfun.("radiofrance: la radio \"#{station}\" n'existe pas")
 
       {:error, %HTTPoison.Response{status_code: code}} ->
         m.replyfun.("radiofrance: erreur http #{code}")
 
       _ ->
         m.replyfun.("radiofrance: ça n'a pas marché, rip")
     end
   end
 
   defp np_url(station), do: "https://www.radiofrance.fr/api/v2.0/stations/#{station}/live"
 
   defp unalias("inter"), do: "franceinter"
   defp unalias("info"), do: "franceinfo"
   defp unalias("bleu"), do: "francebleu"
   defp unalias("culture"), do: "franceculture"
   defp unalias("musique"), do: "francemusique"
   defp unalias(station), do: station
 
   defp format_title(_, nil, nil) do
     nil
   end
   defp format_title(true, title, artist) do
     [artist, title] |> Enum.filter(& &1) |> Enum.join(" - ")
   end
   defp format_title(false, show, section) do
     [show, section] |> Enum.filter(& &1) |> Enum.join(": ")
   end
 
   defp reformat_station_name(station) do
     station
     |> String.replace("france", "france ")
     |> String.replace("_", " ")
   end
 
 end
diff --git a/lib/plugins/seen.ex b/lib/plugins/seen.ex
index cdebd59..045702c 100644
--- a/lib/plugins/seen.ex
+++ b/lib/plugins/seen.ex
@@ -1,59 +1,59 @@
 defmodule Nola.Plugins.Seen do
   @moduledoc """
   # seen
 
   * **!seen `<nick>`**
   """
 
   def irc_doc, do: @moduledoc
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def init([]) do
     regopts = [plugin: __MODULE__]
     {:ok, _} = Registry.register(Nola.PubSub, "triggers", regopts)
     {:ok, _} = Registry.register(Nola.PubSub, "messages", regopts)
     dets_filename = (Nola.data_path() <> "/seen.dets") |> String.to_charlist()
     {:ok, dets} = :dets.open_file(dets_filename, [])
     {:ok, %{dets: dets}}
   end
 
-  def handle_info({:irc, :trigger, "seen", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [nick]}}}, state) do
+  def handle_info({:irc, :trigger, "seen", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: [nick]}}}, state) do
     witness(m, state)
     m.replyfun.(last_seen(m.channel, nick, state))
     {:noreply, state}
   end
 
   def handle_info({:irc, :trigger, _, m}, state) do
     witness(m, state)
     {:noreply, state}
   end
 
   def handle_info({:irc, :text, m}, state) do
     witness(m, state)
     {:noreply, state}
   end
 
-  defp witness(%IRC.Message{channel: channel, text: text, sender: %{nick: nick}}, %{dets: dets}) do
+  defp witness(%Nola.Message{channel: channel, text: text, sender: %{nick: nick}}, %{dets: dets}) do
     :dets.insert(dets, {{channel, nick}, DateTime.utc_now(), text})
     :ok
   end
 
   defp last_seen(channel, nick, %{dets: dets}) do
     case :dets.lookup(dets, {channel, nick}) do
       [{_, date, text}] ->
         diff = round(DateTime.diff(DateTime.utc_now(), date)/60)
         cond do
           diff >= 30 ->
             duration = Timex.Duration.from_minutes(diff)
             format = Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
             "#{nick} a parlé pour la dernière fois il y a #{format}: “#{text}”"
           true -> "#{nick} est là..."
         end
       [] ->
         "je ne connais pas de #{nick}"
     end
   end
 
 end
diff --git a/lib/plugins/sms.ex b/lib/plugins/sms.ex
index afc1eb1..a3b7b7d 100644
--- a/lib/plugins/sms.ex
+++ b/lib/plugins/sms.ex
@@ -1,165 +1,165 @@
 defmodule Nola.Plugins.Sms do
   @moduledoc """
   ## sms
 
   * **!sms `<nick>` `<message>`** envoie un SMS.
   """
   def short_irc_doc, do: false
   def irc_doc, do: @moduledoc
   require Logger
 
   def incoming(from, "enable "<>key) do
     key = String.trim(key)
     account = Nola.Account.find_meta_account("sms-validation-code", String.downcase(key))
     if account do
       net = Nola.Account.get_meta(account, "sms-validation-target")
       Nola.Account.put_meta(account, "sms-number", from)
       Nola.Account.delete_meta(account, "sms-validation-code")
       Nola.Account.delete_meta(account, "sms-validation-number")
       Nola.Account.delete_meta(account, "sms-validation-target")
       IRC.Connection.broadcast_message(net, account, "SMS Number #{from} added!")
       send_sms(from, "Yay! Number linked to account #{account.name}")
     end
   end
 
   def incoming(from, message) do
     account = Nola.Account.find_meta_account("sms-number", from)
     if account do
       reply_fun = fn(text) ->
         send_sms(from, text)
       end
       trigger_text = if Enum.any?(IRC.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(message, trigger) end) do
         message
       else
         "!"<>message
       end
-      message = %IRC.Message{
+      message = %Nola.Message{
         id: FlakeId.get(),
         transport: :sms,
         network: "sms",
         channel: nil,
         text: message,
         account: account,
         sender: %ExIRC.SenderInfo{nick: account.name},
         replyfun: reply_fun,
         trigger: IRC.Connection.extract_trigger(trigger_text)
       }
       Logger.debug("converted sms to message: #{inspect message}")
       IRC.Connection.publish(message, ["messages:sms"])
       message
     end
   end
 
   def my_number() do
     Keyword.get(Application.get_env(:nola, :sms, []), :number, "+33000000000")
   end
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def path() do
     account = Keyword.get(Application.get_env(:nola, :sms), :account)
     "https://eu.api.ovh.com/1.0/sms/#{account}"
   end
 
   def path(rest) do
     Path.join(path(), rest)
   end
 
   def send_sms(number, text) do
     url = path("/virtualNumbers/#{my_number()}/jobs")
     body = %{
       "message" => text,
       "receivers" => [number],
       #"senderForResponse" => true,
       #"noStopClause" => true,
       "charset" => "UTF-8",
       "coding" => "8bit"
     } |> Poison.encode!()
     headers = [{"content-type", "application/json"}] ++ sign("POST", url, body)
     options = []
     case HTTPoison.post(url, body, headers, options) do
       {:ok, %HTTPoison.Response{status_code: 200}} -> :ok
       {:ok, %HTTPoison.Response{status_code: code} = resp} ->
         Logger.error("SMS Error: #{inspect resp}")
         {:error, code}
       {:error, error} -> {:error, error}
     end
   end
 
   def init([]) do
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:sms", [plugin: __MODULE__])
     :ok = register_ovh_callback()
     {:ok, %{}}
     :ignore
   end
 
-  def handle_info({:irc, :trigger, "sms", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [nick | text]}}}, state) do
+  def handle_info({:irc, :trigger, "sms", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: [nick | text]}}}, state) do
     with \
         {:tree, false} <- {:tree, m.sender.nick == "Tree"},
         {_, %Nola.Account{} = account} <- {:account, Nola.Account.find_always_by_nick(m.network, m.channel, nick)},
         {_, number} when not is_nil(number) <- {:number, Nola.Account.get_meta(account, "sms-number")}
     do
       text = Enum.join(text, " ")
       sender = if m.channel do
         "#{m.channel} <#{m.sender.nick}> "
       else
         "<#{m.sender.nick}> "
       end
       case send_sms(number, sender<>text) do
         :ok -> m.replyfun.("sent!")
         {:error, error} -> m.replyfun.("not sent, error: #{inspect error}")
       end
     else
       {:tree, _} -> m.replyfun.("Tree: va en enfer")
       {:account, _} -> m.replyfun.("#{nick} not known")
       {:number, _} -> m.replyfun.("#{nick} have not enabled sms")
     end
     {:noreply, state}
   end
 
   def handle_info(msg, state) do
     {:noreply, state}
   end
 
   defp register_ovh_callback() do
     url = path()
     body = %{
       "callBack" =>NolaWeb.Router.Helpers.sms_url(NolaWeb.Endpoint, :ovh_callback),
       "smsResponse" => %{
         "cgiUrl" => NolaWeb.Router.Helpers.sms_url(NolaWeb.Endpoint, :ovh_callback),
         "responseType" => "cgi"
       }
     } |> Poison.encode!()
     headers = [{"content-type", "application/json"}] ++ sign("PUT", url, body)
     options = []
     case HTTPoison.put(url, body, headers, options) do
       {:ok, %HTTPoison.Response{status_code: 200}} ->
         :ok
       error -> error
     end
   end
 
   defp sign(method, url, body) do
     ts = DateTime.utc_now() |> DateTime.to_unix()
     as = env(:app_secret)
     ck = env(:consumer_key)
     sign = Enum.join([as, ck, String.upcase(method), url, body, ts], "+")
     sign_hex = :crypto.hash(:sha, sign) |> Base.encode16(case: :lower)
     headers = [{"X-OVH-Application", env(:app_key)}, {"X-OVH-Timestamp", ts},
       {"X-OVH-Signature", "$1$"<>sign_hex}, {"X-Ovh-Consumer", ck}]
   end
 
   def parse_number(num) do
     {:error, :todo}
   end
 
   defp env() do
     Application.get_env(:nola, :sms)
   end
 
   defp env(key) do
     Keyword.get(env(), key)
   end
 end
diff --git a/lib/plugins/tell.ex b/lib/plugins/tell.ex
index 43da9e7..bc1f24e 100644
--- a/lib/plugins/tell.ex
+++ b/lib/plugins/tell.ex
@@ -1,106 +1,106 @@
 defmodule Nola.Plugins.Tell do
   use GenServer
 
   @moduledoc """
   # Tell
 
   * **!tell `<nick>` `<message>`**: tell `message` to `nick` when they reconnect.
   """
 
   def irc_doc, do: @moduledoc
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def dets do
     (Nola.data_path() <> "/tell.dets") |> String.to_charlist()
   end
 
   def tell(m, target, message) do
     GenServer.cast(__MODULE__, {:tell, m, target, message})
   end
 
   def init([]) do
     regopts = [plugin: __MODULE__]
     {:ok, _} = Registry.register(Nola.PubSub, "account", regopts)
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:tell", regopts)
     {:ok, dets} = :dets.open_file(dets(), [type: :bag])
     {:ok, %{dets: dets}}
   end
 
   def handle_cast({:tell, m, target, message}, state) do
     do_tell(state, m, target, message)
     {:noreply, state}
   end
 
-  def handle_info({:irc, :trigger, "tell", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [target | message]}}}, state) do
+  def handle_info({:irc, :trigger, "tell", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: [target | message]}}}, state) do
     do_tell(state, m, target, message)
     {:noreply, state}
   end
 
   def handle_info({:account, network, channel, nick, account_id}, state) do
     messages = :dets.lookup(state.dets, {network, channel, account_id})
     if messages != [] do
       strs = Enum.map(messages, fn({_, from, message, at}) ->
         account = Nola.Account.get(from)
         user = Nola.UserTrack.find_by_account(network, account)
         fromnick = if user, do: user.nick, else: account.name
         "#{nick}: <#{fromnick}> #{message}"
       end)
       Enum.each(strs, fn(s) -> IRC.Connection.broadcast_message(network, channel, s) end)
       :dets.delete(state.dets, {network, channel, account_id})
     end
     {:noreply, state}
   end
 
   def handle_info({:account_change, old_id, new_id}, state) do
     #:ets.fun2ms(fn({ {_net, _chan, target_id}, from_id, _, _} = obj) when (target_id == old_id) or (from_id == old_id) -> obj end)
     spec = [{{{:"$1", :"$2", :"$3"}, :"$4", :_, :_}, [{:orelse, {:==, :"$3", {:const, old_id}}, {:==, :"$4", {:const, old_id}}}], [:"$_"]}]
     Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
       case obj do
         { {net, chan, ^old_id}, from_id, message, at } = obj ->
           :dets.delete(obj)
           :dets.insert(table, {{net, chan, new_id}, from_id, message, at})
         {key, ^old_id, message, at} = obj ->
           :dets.delete(table, obj)
           :dets.insert(table, {key, new_id, message, at})
         _ -> :ok
       end
     end)
     {:noreply, state}
   end
 
 
   def handle_info(info, state) do
     {:noreply, state}
   end
 
   def terminate(_, state) do
     :dets.close(state.dets)
     :ok
   end
 
   defp do_tell(state, m, nick_target, message) do
     target = Nola.Account.find_always_by_nick(m.network, m.channel, nick_target)
     message = Enum.join(message, " ")
     with \
          {:target, %Nola.Account{} = target} <- {:target, target},
          {:same, false} <- {:same, target.id == m.account.id},
         target_user = Nola.UserTrack.find_by_account(m.network, target),
         target_nick = if(target_user, do: target_user.nick, else: target.name),
         present? = if(target_user, do: Map.has_key?(target_user.last_active, m.channel)),
          {:absent, true, _} <- {:absent, !present?, target_nick},
          {:message, message} <- {:message, message}
     do
       obj = { {m.network, m.channel, target.id}, m.account.id, message, NaiveDateTime.utc_now()}
       :dets.insert(state.dets, obj)
       m.replyfun.("will tell to #{target_nick}")
     else
       {:same, _} -> m.replyfun.("are you so stupid that you need a bot to tell yourself things ?")
       {:target, _} -> m.replyfun.("#{nick_target} unknown")
       {:absent, _, nick} -> m.replyfun.("#{nick} is here, tell yourself!")
       {:message, _} -> m.replyfun.("can't tell without a message")
     end
   end
 
 end
diff --git a/lib/plugins/untappd.ex b/lib/plugins/untappd.ex
index e1731bd..e409172 100644
--- a/lib/plugins/untappd.ex
+++ b/lib/plugins/untappd.ex
@@ -1,66 +1,66 @@
 defmodule Nola.Plugins.Untappd do
 
   def irc_doc() do
     """
     # [Untappd](https://untappd.com)
 
     * `!beer <beer name>` Information about the first beer matching `<beer name>`
     * `?beer <beer name>` List the 10 firsts beer matching `<beer name>`
 
     _Note_: The best way to search is always "Brewery Name + Beer Name", such as "Dogfish 60 Minute".
 
     Link your Untappd account to the bot (for automated checkins on [alcoolog](#alcoolog), ...) with the `enable-untappd` command, in private.
     """
   end
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def init(_) do
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:beer", [plugin: __MODULE__])
     {:ok, %{}}
   end
 
-  def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %{type: :bang, args: args}}}, state) do
+  def handle_info({:irc, :trigger, _, m = %Nola.Message{trigger: %{type: :bang, args: args}}}, state) do
     case Untappd.search_beer(Enum.join(args, " "), limit: 1) do
       {:ok, %{"response" => %{"beers" => %{"count" => count, "items" => [result | _]}}}} ->
         %{"beer" => beer, "brewery" => brewery} = result
         description = Map.get(beer, "beer_description")
                       |> String.replace("\n", " ")
                       |> String.replace("\r", " ")
                       |> String.trim()
         beer_s = "#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")} - #{Map.get(beer, "beer_abv")}°"
         city = get_in(brewery, ["location", "brewery_city"])
         location = [Map.get(brewery, "brewery_type"), city, Map.get(brewery, "country_name")]
                    |> Enum.filter(fn(x) -> x end)
                    |> Enum.join(", ")
         extra = "#{Map.get(beer, "beer_style")} - IBU: #{Map.get(beer, "beer_ibu")} - #{location}"
         m.replyfun.([beer_s, extra, description])
       err ->
         m.replyfun.("Error")
     end
     {:noreply, state}
   end
 
 
-  def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %{type: :query, args: args}}}, state) do
+  def handle_info({:irc, :trigger, _, m = %Nola.Message{trigger: %{type: :query, args: args}}}, state) do
     case Untappd.search_beer(Enum.join(args, " ")) do
       {:ok, %{"response" => %{"beers" => %{"count" => count, "items" => results}}}} ->
         beers = for %{"beer" => beer, "brewery" => brewery} <- results do
           "#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")} - #{Map.get(beer, "beer_abv")}°"
         end
         |> Enum.intersperse(", ")
         |> Enum.join("")
         m.replyfun.("#{count}. #{beers}")
       err ->
         m.replyfun.("Error")
     end
     {:noreply, state}
   end
 
   def handle_info(info, state) do
     {:noreply, state}
   end
 
 end
diff --git a/lib/plugins/user_mention.ex b/lib/plugins/user_mention.ex
index f26f1d6..e7c7420 100644
--- a/lib/plugins/user_mention.ex
+++ b/lib/plugins/user_mention.ex
@@ -1,52 +1,52 @@
 defmodule Nola.Plugins.UserMention do
   @moduledoc """
   # mention
 
   * **@`<nick>` `<message>`**: notifie si possible le nick immédiatement via Telegram, SMS, ou équivalent à `!tell`.
   """
 
   require Logger
 
   def short_irc_doc, do: false
   def irc_doc, do: @moduledoc
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def init(_) do
     {:ok, _} = Registry.register(Nola.PubSub, "triggers", plugin: __MODULE__)
     {:ok, nil}
   end
 
-  def handle_info({:irc, :trigger, nick, message = %IRC.Message{sender: sender, account: account, network: network, channel: channel, trigger: %IRC.Trigger{type: :at, args: content}}}, state) do
+  def handle_info({:irc, :trigger, nick, message = %Nola.Message{sender: sender, account: account, network: network, channel: channel, trigger: %Nola.Trigger{type: :at, args: content}}}, state) do
     nick = nick
     |> String.trim(":")
     |> String.trim(",")
     target = Nola.Account.find_always_by_nick(network, channel, nick)
     if target do
       telegram = Nola.Account.get_meta(target, "telegram-id")
       sms = Nola.Account.get_meta(target, "sms-number")
       text = "#{channel} <#{sender.nick}> #{Enum.join(content, " ")}"
 
       cond do
         telegram ->
           Nola.Telegram.send_message(telegram, "`#{channel}` <**#{sender.nick}**> #{Enum.join(content, " ")}")
         sms ->
           case Nola.Plugins.Sms.send_sms(sms, text) do
             {:error, code} -> message.replyfun("#{sender.nick}: erreur #{code} (sms)")
           end
         true ->
           Nola.Plugins.Tell.tell(message, nick, content)
       end
     else
       message.replyfun.("#{nick} m'est inconnu")
     end
     {:noreply, state}
   end
 
   def handle_info(_, state) do
     {:noreply, state}
   end
 
 end
diff --git a/lib/plugins/wikipedia.ex b/lib/plugins/wikipedia.ex
index caef306..47b14da 100644
--- a/lib/plugins/wikipedia.ex
+++ b/lib/plugins/wikipedia.ex
@@ -1,90 +1,90 @@
 defmodule Nola.Plugins.Wikipedia do
   require Logger
 
   @moduledoc """
   # wikipédia
 
   * **!wp `<recherche>`**: retourne le premier résultat de la `<recherche>` Wikipedia
   * **!wp**: un article Wikipédia au hasard
   """
 
   def irc_doc, do: @moduledoc
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def init(_) do
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:wp", [plugin: __MODULE__])
     {:ok, nil}
   end
 
-  def handle_info({:irc, :trigger, _, message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: []}}}, state) do
+  def handle_info({:irc, :trigger, _, message = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: []}}}, state) do
     irc_random(message)
     {:noreply, state}
   end
-  def handle_info({:irc, :trigger, _, message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) do
+  def handle_info({:irc, :trigger, _, message = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}}, state) do
     irc_search(Enum.join(args, " "), message)
     {:noreply, state}
   end
 
   def handle_info(info, state) do
     {:noreply, state}
   end
 
   defp irc_search("", message), do: irc_random(message)
   defp irc_search(query, message) do
     params = %{
       "action" => "query",
       "list" => "search",
       "srsearch" => String.strip(query),
       "srlimit" => 1,
     }
     case query_wikipedia(params) do
       {:ok, %{"query" => %{"search" => [item | _]}}} ->
         title = item["title"]
         url = "https://fr.wikipedia.org/wiki/" <> String.replace(title, " ", "_")
         msg = "Wikipédia: #{title} — #{url}"
         message.replyfun.(msg)
       _ ->
         nil
     end
   end
 
   defp irc_random(message) do
     params = %{
       "action" => "query",
       "generator" => "random",
       "grnnamespace" => 0,
       "prop" => "info"
     }
     case query_wikipedia(params) do
       {:ok, %{"query" => %{"pages" => map = %{}}}} ->
         [{_, item}] = Map.to_list(map)
         title = item["title"]
         url = "https://fr.wikipedia.org/wiki/" <> String.replace(title, " ", "_")
         msg = "Wikipédia: #{title} — #{url}"
         message.replyfun.(msg)
       _ ->
         nil
     end
   end
 
   defp query_wikipedia(params) do
     url = "https://fr.wikipedia.org/w/api.php"
     params = params
     |> Map.put("format", "json")
     |> Map.put("utf8", "")
 
     case HTTPoison.get(url, [], params: params) do
       {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> Jason.decode(body)
       {:ok, %HTTPoison.Response{status_code: 400, body: body}} ->
         Logger.error "Wikipedia HTTP 400: #{inspect body}"
         {:error, "http 400"}
       error ->
         Logger.error "Wikipedia http error: #{inspect error}"
         {:error, "http client error"}
     end
   end
 
 end
diff --git a/lib/plugins/wolfram_alpha.ex b/lib/plugins/wolfram_alpha.ex
index 02c1c51..120af16 100644
--- a/lib/plugins/wolfram_alpha.ex
+++ b/lib/plugins/wolfram_alpha.ex
@@ -1,47 +1,47 @@
 defmodule Nola.Plugins.WolframAlpha do
   use GenServer
   require Logger
 
   @moduledoc """
   # wolfram alpha
 
   * **`!wa <requête>`** lance `<requête>` sur WolframAlpha
   """
 
   def irc_doc, do: @moduledoc
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def init(_) do
     {:ok, _} = Registry.register(Nola.PubSub, "trigger:wa", [plugin: __MODULE__])
     {:ok, nil}
   end
 
-  def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: query}}}, state) do
+  def handle_info({:irc, :trigger, _, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: query}}}, state) do
     query = Enum.join(query, " ")
     params = %{
       "appid" => Keyword.get(Application.get_env(:nola, :wolframalpha, []), :app_id, "NO_APP_ID"),
       "units" => "metric",
       "i" => query
     }
     url = "https://www.wolframalpha.com/input/?i=" <> URI.encode(query)
     case HTTPoison.get("http://api.wolframalpha.com/v1/result", [], [params: params]) do
       {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
         m.replyfun.(["#{query} -> #{body}", url])
       {:ok, %HTTPoison.Response{status_code: code, body: body}} ->
         error = case {code, body} do
           {501, b} -> "input invalide: #{body}"
           {code, error} -> "erreur #{code}: #{body || ""}"
         end
         m.replyfun.("wa: #{error}")
       {:error, %HTTPoison.Error{reason: reason}} ->
         m.replyfun.("wa: erreur http: #{to_string(reason)}")
       _ ->
         m.replyfun.("wa: erreur http")
     end
     {:noreply, state}
   end
 
 end
diff --git a/lib/plugins/youtube.ex b/lib/plugins/youtube.ex
index e23fd45..39bf03d 100644
--- a/lib/plugins/youtube.ex
+++ b/lib/plugins/youtube.ex
@@ -1,104 +1,104 @@
 defmodule Nola.Plugins.YouTube do
   require Logger
 
   @moduledoc """
   # youtube
 
   * **!yt `<recherche>`**, !youtube `<recherche>`: retourne le premier résultat de la `<recherche>` YouTube
   """
 
   defstruct client: nil
 
   def irc_doc, do: @moduledoc
 
   def start_link() do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
   def init([]) do
     for t <- ["trigger:yt", "trigger:youtube"], do: {:ok, _} = Registry.register(Nola.PubSub, t, [plugin: __MODULE__])
     {:ok, %__MODULE__{}}
   end
 
-  def handle_info({:irc, :trigger, _, message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) do
+  def handle_info({:irc, :trigger, _, message = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}}, state) do
     irc_search(Enum.join(args, " "), message)
     {:noreply, state}
   end
 
   def handle_info(info, state) do
     {:noreply, state}
   end
 
   defp irc_search(query, message) do
     case search(query) do
       {:ok, %{"items" => [item | _]}} ->
         url = "https://youtube.com/watch?v=" <> item["id"]
         snippet = item["snippet"]
         duration = item["contentDetails"]["duration"] |> String.replace("PT", "") |> String.downcase
         date = snippet["publishedAt"]
                |> DateTime.from_iso8601()
                |> elem(1)
                |> Timex.format("{relative}", :relative)
                |> elem(1)
 
         info_line = "— #{duration} — uploaded by #{snippet["channelTitle"]} — #{date}"
              <> " — #{item["statistics"]["viewCount"]} views, #{item["statistics"]["likeCount"]} likes,"
              <> " #{item["statistics"]["dislikeCount"]} dislikes"
         message.replyfun.("#{snippet["title"]} — #{url}")
         message.replyfun.(info_line)
       {:error, error} ->
         message.replyfun.("Erreur YouTube: "<>error)
       _ ->
         nil
     end
   end
 
   defp search(query) do
     query = query
     |> String.strip
     key = Application.get_env(:nola, :youtube)[:api_key]
     params = %{
       "key" => key,
       "maxResults" => 1,
       "part" => "id",
       "safeSearch" => "none",
       "type" => "video",
       "q" => query,
     }
     url = "https://www.googleapis.com/youtube/v3/search"
     case HTTPoison.get(url, [], params: params) do
       {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
         {:ok, json} = Jason.decode(body)
         item = List.first(json["items"])
         if item do
           video_id = item["id"]["videoId"]
           params = %{
             "part" => "snippet,contentDetails,statistics",
             "id" => video_id,
             "key" => key
           }
           headers = []
           options = [params: params]
           case HTTPoison.get("https://www.googleapis.com/youtube/v3/videos", [], options) do
             {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
               Jason.decode(body)
             {:ok, %HTTPoison.Response{status_code: code, body: body}} ->
               Logger.error "YouTube HTTP #{code}: #{inspect body}"
               {:error, "http #{code}"}
             error ->
               Logger.error "YouTube http error: #{inspect error}"
               :error
           end
         else
           :error
         end
       {:ok, %HTTPoison.Response{status_code: code, body: body}} ->
         Logger.error "YouTube HTTP #{code}: #{inspect body}"
         {:error, "http #{code}"}
       error ->
         Logger.error "YouTube http error: #{inspect error}"
         :error
     end
   end
 
 end
diff --git a/lib/telegram/room.ex b/lib/telegram/room.ex
index cc10e90..8e95ca8 100644
--- a/lib/telegram/room.ex
+++ b/lib/telegram/room.ex
@@ -1,188 +1,188 @@
 defmodule Nola.TelegramRoom do
   require Logger
   @behaviour Telegram.ChatBot
   alias Telegram.Api
 
   @couch "bot-telegram-rooms"
 
   def rooms(), do: rooms(:with_docs)
 
   @spec rooms(:with_docs | :ids) :: [Map.t | integer( )]
   def rooms(:with_docs) do
     case Couch.get(@couch, :all_docs, include_docs: true) do
       {:ok, %{"rows" => rows}} -> {:ok, for(%{"doc" => doc} <- rows, do: doc)}
       error = {:error, _} -> error
     end
   end
 
   def rooms(:ids) do
     case Couch.get(@couch, :all_docs) do
       {:ok, %{"rows" => rows}} -> {:ok, for(%{"id" => id} <- rows, do: id)}
       error = {:error, _} -> error
     end
   end
 
   def room(id, opts \\ []) do
     Couch.get(@couch, id, opts)
   end
 
   # TODO: Create couch
   def setup() do
     :ok
   end
 
   def after_start() do
     for id <- room(:ids), do: Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(Nola.Telegram, Integer.parse(id) |> elem(0))
   end
 
   @impl Telegram.ChatBot
   def init(id) when is_integer(id) and id < 0 do
     token = Keyword.get(Application.get_env(:nola, :telegram, []), :key)
     {:ok, chat} = Api.request(token, "getChat", chat_id: id)
     Logger.metadata(transport: :telegram, id: id, telegram_room_id: id)
     tg_room = case room(id) do
       {:ok, tg_room = %{"network" => _net, "channel" => _chan}} -> tg_room
       {:error, :not_found} ->
         [net, chan] = String.split(chat["title"], "/", parts: 2)
         {net, chan} = case IRC.Connection.get_network(net, chan) do
           %IRC.Connection{} -> {net, chan}
           _ -> {nil, nil}
         end
         {:ok, _id, _rev} = Couch.post(@couch, %{"_id" => id, "network" => net, "channel" => nil})
         {:ok, tg_room} = room(id)
         tg_room
     end
     %{"network" => net, "channel" => chan} = tg_room
     Logger.info("Starting ChatBot for room #{id} \"#{chat["title"]}\" #{inspect tg_room}")
     irc_plumbed = if net && chan do
         {:ok, _} = Registry.register(Nola.PubSub, "#{net}/#{chan}:messages", plugin: __MODULE__)
         {:ok, _} = Registry.register(Nola.PubSub, "#{net}/#{chan}:triggers", plugin: __MODULE__)
         {:ok, _} = Registry.register(Nola.PubSub, "#{net}/#{chan}:outputs", plugin: __MODULE__)
         true
     else
       Logger.warn("Did not found telegram match for #{id} \"#{chat["title"]}\"")
       false
     end
     {:ok, %{id: id, net: net, chan: chan, irc: irc_plumbed}}
   end
 
   def init(id) do
     Logger.error("telegram_room: bad id (not room id)", transport: :telegram, id: id, telegram_room_id: id)
     :ignoree
   end
 
   defp find_or_create_meta_account(from = %{"id" => user_id}, state) do
     if account = Nola.Account.find_meta_account("telegram-id", user_id) do
       account
     else
       first_name = Map.get(from, "first_name")
       last_name = Map.get(from, "last_name")
       name = [first_name, last_name]
       |> Enum.filter(& &1)
       |> Enum.join(" ")
 
       username = Map.get(from, "username", first_name)
 
       account = username
       |> Nola.Account.new_account()
       |> Nola.Account.update_account_name(name)
       |> Nola.Account.put_meta("telegram-id", user_id)
 
       Logger.info("telegram_room: created account #{account.id} for telegram user #{user_id}")
       account
     end
   end
 
   def handle_update(%{"message" => %{"from" => from = %{"id" => user_id}, "text" => text}}, _token, state) do
     account = find_or_create_meta_account(from, state)
     connection = IRC.Connection.get_network(state.net)
     IRC.send_message_as(account, state.net, state.chan, text, true)
     {:ok, state}
   end
 
   def handle_update(data = %{"message" => %{"from" => from = %{"id" => user_id}, "location" => %{"latitude" => lat, "longitude" => lon}}}, _token, state) do
     account = find_or_create_meta_account(from, state)
     connection = IRC.Connection.get_network(state.net)
     IRC.send_message_as(account, state.net, state.chan, "@ #{lat}, #{lon}", true)
     {:ok, state}
   end
 
   for type <- ~w(photo voice video document animation) do
     def handle_update(data = %{"message" => %{unquote(type) => _}}, token, state) do
       upload(unquote(type), data, token, state)
     end
   end
 
   def handle_update(update, token, state) do
     {:ok, state}
   end
 
   def handle_info({:irc, _, _, message}, state) do
     handle_info({:irc, nil, message}, state)
   end
 
-  def handle_info({:irc, _, message = %IRC.Message{sender: %{nick: nick}, text: text}}, state) do
+  def handle_info({:irc, _, message = %Nola.Message{sender: %{nick: nick}, text: text}}, state) do
     if Map.get(message.meta, :from) == self() do
     else
       body = if Map.get(message.meta, :self), do: text, else: "<#{nick}> #{text}"
       Nola.Telegram.send_message(state.id, body)
     end
     {:ok, state}
   end
 
   def handle_info(info, state) do
     Logger.info("UNhandled #{inspect info}")
     {:ok, state}
   end
 
   defp upload(_type, %{"message" => m = %{"chat" => %{"id" => chat_id}, "from" => from = %{"id" => user_id}}}, token, state) do
     account = find_or_create_meta_account(from, state)
     if account do
       {content, type} = cond do
         m["photo"] -> {m["photo"], "photo"}
         m["voice"] -> {m["voice"], "voice message"}
         m["video"] -> {m["video"], "video"}
         m["document"] -> {m["document"], "file"}
         m["animation"] -> {m["animation"], "gif"}
       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(m["caption"], do: m["caption"] <> " ", else: "")
 
       spawn(fn() ->
         with \
           {:ok, file} <- Telegram.Api.request(token, "getFile", file_id: file_id),
           path = "https://api.telegram.org/file/bot#{token}/#{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}",
           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}"
           txt = "#{type}: #{text}#{path}"
           connection = IRC.Connection.get_network(state.net)
           IRC.send_message_as(account, state.net, state.chan, txt, true)
         else
           error ->
             Telegram.Api.request(token, "sendMessage", chat_id: chat_id, text: "File upload failed, sorry.")
             Logger.error("Failed upload from Telegram: #{inspect error}")
         end
       end)
 
       {:ok, state}
     end
   end
 
 end
diff --git a/lib/telegram/telegram.ex b/lib/telegram/telegram.ex
index dd23146..9a2812d 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"] -> 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{
+      message = %Nola.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 = 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/tmpl.ex b/lib/tmpl.ex
index 62c2a46..e4489ac 100644
--- a/lib/tmpl.ex
+++ b/lib/tmpl.ex
@@ -1,124 +1,124 @@
 defmodule Tmpl do
   require Logger
 
   defmodule Filter do
     use Liquex.Filter
 
     def repeat(text, val, _) do
       String.duplicate(text, val)
     end
 
     def rrepeat(text, max, _) do
       String.duplicate(text, :random.uniform(max))
     end
 
     def rrepeat(text, var) do
       rrepeat(text, 20, var)
     end
 
     def bold(text, %{variables: variables}) do
       unless Map.get(variables, "_no_format") || Map.get(variables, "_no_bold") do
         <<2>> <> text <> <<2>>
       else
         text
       end
     end
 
     @colors [:white, :black, :blue, :green, :red, :brown, :purple, :orange, :yellow, :light_green, :cyan, :light_blue, :pink, :grey, :light_grey]
 
     for {color, index} <- Enum.with_index(@colors) do
       code = 48+index
 
       def color_code(unquote(color)) do
         unquote(code)
       end
 
       def unquote(color)(text, %{variables: variables}) do
         unless Map.get(variables, "_no_format") || Map.get(variables, "_no_colors") do
           <<3, unquote(code)>> <> text <> <<3>>
         else
           text
         end
       end
     end
 
     def account_nick(%{"id" => id, "name" => name}, %{variables: %{"message" => %{"network" => network}}}) do
       if user = Nola.UserTrack.find_by_account(network, %Nola.Account{id: id}) do
         user.nick
       else
         name
       end
     end
 
     def account_nick(val, ctx) do
       "{{account_nick}}"
     end
 
   end
 
-  def render(template, msg = %IRC.Message{}, context \\ %{}, safe \\ true) do
+  def render(template, msg = %Nola.Message{}, context \\ %{}, safe \\ true) do
     do_render(template, Map.put(context, "message", msg), safe)
   end
 
   defp do_render(template, context, safe) when is_binary(template) do
     case Liquex.parse(template) do
       {:ok, template_ast} ->
         do_render(template_ast, context, safe)
       {:error, err, pos} ->
         Logger.debug("Liquid error: #{pos} - #{inspect template}")
         "[liquid ast error (at #{pos}): #{inspect err}]"
     end
   end
 
   defp do_render(template_ast, context, safe) when is_list(template_ast) do
     context = Liquex.Context.new(mapify(context, safe))
               |> Map.put(:filter_module, Tmpl.Filter)
     {content, _context} = Liquex.render(template_ast, context)
     to_string(content)
   rescue
     e ->
       Logger.error("Liquid error: #{inspect e}")
       "[liquid rendering error]"
   end
 
   defp mapify(struct = %{__struct__: _}, safe) do
     mapify(Map.from_struct(struct), safe)
   end
 
   defp mapify(map = %{}, safe) do
     map
     |> Enum.reduce(Map.new, fn({k,v}, acc) ->
       k = to_string(k)
       if safe?(k, safe) do
         if v = mapify(v, safe) do
           Map.put(acc, k, v)
         else
           acc
         end
       else
         acc
       end
     end)
   end
 
   defp mapify(fun, _) when is_function(fun) do
     nil
   end
 
   defp mapify(atom, _) when is_atom(atom) do
     to_string(atom)
   end
 
   defp mapify(v, _) do
     v
   end
 
   defp safe?(_, false) do
     true
   end
 
   defp safe?("token", true), do: false
   defp safe?("password", true), do: false
   defp safe?(_, true), do: true
 end
 
diff --git a/lib/web/live/chat_live.html.heex b/lib/web/live/chat_live.html.heex
index 29cd6a1..470604f 100644
--- a/lib/web/live/chat_live.html.heex
+++ b/lib/web/live/chat_live.html.heex
@@ -1,91 +1,91 @@
 <div class="chat" data-turbo="false">
 
   <div class="py-4 px-4 bg-gradient-to-b from-black to-gray-900">
     <div class="grid grid-cols-2">
       <h1 class="text-gray-50 tracking-tight font-extrabold text-xl">
         <%= @network %>
         <span class="font-bold"><%= @chan %></span>
       </h1>
       <div class="text-right">
         <a href="/" class="text-gray-400"><%= @account_id %></a>
       </div>
     </div>
   </div>
 
   <div class="body">
 
     <div class="log">
       <p class="disconnected text-center text-6xl tracking-tight font-extrabold text-red-800 w-full my-24 mx-auto overflow-y-auto">
         Disconnected <span class="text-mono">:'(</span>
       </p>
       <p class="phx-errored text-center text-6xl tracking-tight font-extrabold text-red-800 w-full my-24 mx-auto overflow-y-auto">
         Oh no error <span class="text-mono">>:(</span>
       </p>
 
       <ul class="pt-4 pl-4">
           <%= for message <- @backlog do %>
-              <%= if is_map(message) && Map.get(message, :__struct__) == IRC.Message do %>
+              <%= if is_map(message) && Map.get(message, :__struct__) == Nola.Message do %>
               <li class="flex gap-2 place-items-center message"
                   data-account-id={message.account.id}>
                 <NolaWeb.MessageComponent.content
                   message={message}
                       self={message.account.id == @account_id}
                       text={message.text}
                       />
                 </li>
               <% end %>
 
               <%= if is_binary(message) do %>
                 <li class="notice"><%= message %></li>
               <% end %>
 
               <%= if is_map(message) && Map.get(message, :type) do %>
                 <li class="flex gap-2 place-items-center event">
                   <NolaWeb.Component.naive_date_time_utc datetime={message.at} format="time-24-with-seconds" />
                     <span class="inline-block font-bold flex-none cursor-default text-gray-700">*&nbsp;*&nbsp;*</span>
                     <span class="inline-block flex-grow cursor-default text-gray-700">
                       <NolaWeb.EventComponent.content event={message}
                                                      self={@users[message.user_id] && @users[message.user_id].account == @account_id}
                                                      user={@users[message.user_id]}
                                                      />
                     </span>
                   </li>
               <% end %>
           <% end %>
         </ul>
     </div>
 
     <aside>
       <%= for {_, user} <- @users do %>
           <details class="user dropdown">
             <summary><%= user.nick %></summary>
             <div class="content">
               <h3 class="text-xl font-bold"><%= user.nick %></h3>
 
               <ul class="mt-4 space-y-2">
                 <li class="">User: <span class="font-bold"><%= user.username %></span></li>
                 <li class="">Name: <%= user.realname || user.nick %></li>
                 <li class="">Host: <span class="font-mono"><%= user.host %></span></li>
               </ul>
 
               <div class="mt-4 font-xs text-gray-300 text-center">
                 UID: <%= user.id %>
                 <br />
                 AID: <%= user.account %>
               </div>
             </div>
           </details>
       <% end %>
     </aside>
 
   </div>
 
   <.form let={f} id={"form-#{@counter}"} for={:message} phx-submit="send" class="w-full px-4 pt-4">
     <div>
       <div class="mt-1 flex rounded-md shadow-sm border border-gray-300">
           <%= text_input f, :text, class: "focus:ring-indigo-500 focus:border-indigo-500 block w-full border rounded-md pl-4 sm:text-sm border-gray-300", autofocus: true, 'phx-hook': "AutoFocus", autocomplete: "off", placeholder: "Don't be shy, say something…" %>
           <%= submit content_tag(:span, "Send"), class: "-ml-px relative inline-flex items-center space-x-2 px-4 py-2 border border-gray-300 text-sm font-medium rounded-r-md text-gray-700 bg-gray-50 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"%>
       </div>
     </div>
   </.form>
 </div>