Elixir Sips: ejabberd with Elixir – Part 2

Elixir Sips is an Elixir screencast website providing great tutorials to learn Elixir but also to help you build extraordinary pieces of code quickly with Elixir.

They produced a great series of videos on programming ejabberd with Elixir. In the first part of that tutorial, you have learned how to set up Elixir support in ejabberd and write your first ejabberd module in Elixir. Here is the material for the second part in which you will learn how to leverage one of the most powerful ejabberd hooks: ejabberd packet filter.

We will publish more material on ProcessOne blog soon. Stay tuned !

Most of all, please, give us feedback in the comments on what you would like to see covered regarding ejabberd and Elixir… Or even better, come to see us live at upcoming ejabberd San Francisco Bay Area Meetup !

Here is the second ejabberd / Elixir tutorial (Part 2) by Josh Adams.

Introduction

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


Let us know what you think 💬


3 thoughts on “Elixir Sips: ejabberd with Elixir – Part 2

  1. Mike, I’m surprised this hook of ejabberd allows to alter the from and to jid too. I was wondering if this violates any of the XMPP spec and found the following section- RFC 6120 8.3.3.14.Say for example, if the to jid is altered, then XMPP spec mandates to send a redirect error code to the sender. Does ejabberd honor that?

    • ejabberd is compliant with XMPP. However, ejabberd is powerful, pluggable and extensible. You can indeed do bad things in your modules, but this is the responsibility of the module developer. We juste empower developers. It is up to you to do useful, relevant and smart extensions.

Leave a Comment


This site uses Akismet to reduce spam. Learn how your comment data is processed.