In the last episode, we saw how to get notified of presence messages from users connected to the ejabberd server. Now we're going to look at how we can modify messages as they are sent from user to user. Specifically, we're going to build a module that makes everyone yell all the time.

Project

We're going to just start where the last episode left off. Let's add a new module first.

cp lib/mod_presence_demo.ex lib/filter_packet_demo.ex
vim lib/filter_packet_demo.ex
defmodule FilterPacketDemo do
  import Ejabberd.Logger # this allow using info, error, etc for logging
  @behaviour :gen_mod

  def start(_host, _opts) do
    info('Starting ejabberd module Filter Packet Demo')
    # NOTE: The second argument here is global
    Ejabberd.Hooks.add(:filter_packet, :global, __ENV__.module, :on_filter_packet, 50)
    :ok
  end

  def stop(_host) do
    info('Stopping ejabberd module Filter Packet Demo')
    # NOTE: The second argument here is global
    Ejabberd.Hooks.delete(:filter_packet, :global, __ENV__.module, :on_filter_packet, 50)
    :ok
  end

  def on_filter_packet({from, to, xml} = packet) do
    info("Filtering packet: #{inspect {from, to, xml}}")
    packet
  end
end

Now I'll compile it and install it with my ,t mapping.

Now let's enable this module in our server. Open up the config in ~/my-ejabberd and add our new module.

Next let's just start the ejabberd server:

./sbin/ejabberdctl iexlive

We'll connect with two users and chat between them, and we'll see all the packets that flow through ejabberd. Of course, we only really want to do anything to messages, so let's restrict our hook to only catch those:

defmodule FilterPacketDemo do
  def on_filter_packet({from, to, xml={:xmlel, "message", _attributes, _children}} = packet) do
    info("Filtering message: #{inspect packet}")
    body = :xml.get_subtag(xml, "body")
    info(inspect body)
    packet
  end
  def on_filter_packet(packet), do: packet
end

Compile and restart, send some messages, and now we only see info logs on our actual messages...You'll note there's an empty message sent every time we send one with a body as well. I believe this is just the confirmation of receipt of a given message.

We want to just write a filter that will upcase all of these messages, to begin with. To do that, all we would have to do is replace the body with the upcased body. Let's think about how to do that. Basically, we would just want to map the children tags of the message, modifying the cdata if they match a certain tag name, and then use the mapped result as the children of the packet that we pass along. Let's try that.

defmodule FilterPacketDemo do
  import Ejabberd.Logger # this allow using info, error, etc for logging
  @behaviour :gen_mod

  def start(host, _opts) do
    info('Starting ejabberd module Filter Packet Demo')
    Ejabberd.Hooks.add(:filter_packet, :global, __ENV__.module, :on_filter_packet, 50)
    :ok
  end

  def stop(host) do
    info('Stopping ejabberd module Filter Packet Demo')
    Ejabberd.Hooks.delete(:filter_packet, :global, __ENV__.module, :on_filter_packet, 50)
    :ok
  end

  def on_filter_packet({from, to, xml={:xmlel, "message", attributes, children}} = packet) do
    info("Filtering message: #{inspect packet}")

    new_children = Enum.map(children, fn(child) ->
      case child do
        {:xmlel, "body", [], [xmlcdata: text]} ->
          {:xmlel, "body", [], [xmlcdata: String.upcase(text)]}
        _ -> child
      end
    end)

    {from, to, {:xmlel, "message", attributes, new_children}}
  end
  def on_filter_packet(packet), do: packet
end

Go ahead and compile it and restart the server, and let's send some messages.

(( do that, note they are upcased ))

Summary

With that little bit of code, we're able to modify the behaviour of this ejabberd server with respect to messages being sent. Obviously you could imagine how to do more interesting things here, but this was a pretty simple introduction into packet filtering with ejabberd. See you soon!

Resources