All Projects → michaelklishin → merb-messenger

michaelklishin / merb-messenger

Licence: MIT License
Attempt to come up with a useable unified messaging interface for Merb

Programming Languages

ruby
36898 projects - #4 most used programming language
merb_messenger
==============

A plugin for the Merb framework that provides
messaging and notifications functionality.

The goal is to create a simple transport agnostic
library that can be used for Jabber, Growl, HTTP,
mail and other notifications with minimal effort
in web controllers.

Actors
==============

Core actors of this library are messaging controllers
and transports. Messaging controllers are called
from web controllers that handle incoming requests,
render message templates, do messaging-related
filtering and pass the result to transports to deliver.

Note that messaging controllers share rendering and
filtering code with Merb core's AbstractController so
no wheel is invented.

Transports and possible use cases
=====================================

What transports do is up to specific implementation:

* Jabber transport will deliver messages over XMPP
* AMQP transport will talk to AMQP broker like RabbitMQ or ZeroMQ
* mail transport will use SMTP or sendmail to send emails
* HTTP POST transport will post data over HTTP ("web hooks")
* Specific HTTP transport may post to Facebook, Twitter, LiveJournal or whatever it may be
* IRC transport will flood some IRC channel.
* SMS transport will deliver SMS one way or another
* XMP-RPC transport will call some WS-* deathstars
* Some futuristic crazy transport will send Rubinius VM bytecode
  over the wire to be executed on the other end


How to use this prototype
==============================

Messaging controllers are used from web controllers
(that handle requests) and can render templates,
have filters and all that. Transports can be used
directly in models and not supposed to render
(at least at this point in evolution of merb messenger).

Transports must be required explicitly at this point. Do it
in init.rb or add config/messengers.rb and load it from init.rb,
and require transport file there. This is because otherwise
lazy loading can only be accomplished using Kernel#autoload
which some people do not like (for instance, it is not thread
safe in MRI).

Say we have a group journal/blog and we want to be notified
on updates using Jabber. Our web controller action may
look like this:

class Entries < Application
  def create(entry)
    @entry = Entry.new(entry)
    if @entry.save
      run_later do
        JournalMessenger.dispatch(:create, self, :entry => @entry)
      end
      redirect url(:entries)
    else
      @entries = Entry.all
      render :index
    end
  end
end

As you can see, messaging controller is explicitly called from web controller
using dispatch method. That method takes an action as a symbol, web controller
instance (to have access to request parameters and session) and a Hash of extra
options you'd like to pass to it.

Now lets look at our messenger:

class JournalMessenger < Merb::MessagingController
  delivery_options :lan_growl,
                   :notification => "journal notification",
                   :title        => "New journal entry posted"
                   
  delivery_options :remote,
                   :title        => "New journal entry posted",
                   :to           => ["[email protected]"],
                   :message_type => :chat
  
  def create(options)
    # render the template in app/messengers/views/journal/create.text.erb
    body = render(:index)

    # deliver using XMPP
    # transport is called :remote
    # in the application
    deliver :remote,    :body => body
    # deliver using Growl transport
    # that is called :lan_growl across
    # the app
    deliver :lan_growl, :body => body
  end
end

Every messenger action takes a hash of options that includes web controller
parameters and additional options you passed on dispatch.

Every messenger uses +deliver+ method to deliver notifications instantly
and (in the future) +queue+ method to stack a notification in a queue to
be delivered later. Queueing functionality will be 100% ORM agnostic.

This plugin is all about connecting your web controllers and transports.
It supposed to be generic. Because message transports are very
different, it's up to particular transport what
options delivery and queue methods would take.

To eliminate the need to constantly duplicate some common options (like "from",
"title" and similar), Merb messenger lets you set them up messenger-wide:

  delivery_options :remote,
                   :title        => "Journal event",
                   :to           => ["[email protected]"],
                   :message_type => :chat

Above we say that all messages that go via transport called "remote"
(that is XMPP transport in this example) use title "Journal event",
delivered to "[email protected]" and use message type
"chat".

+deliver+ method's options are merged with class-wide delivery options
before they are passed on to transport. Obviously, former take
precedence over the latter.

Implementing transports
========================

Transport API is simple and may be as little work as implementing a
single method, deliver. Some transports that need to establish connection and
handle disconnection may add a few more methods: connect, reconnect, disconnect.
Those providing queuing functionality may add queue method that acts as deliver
but does not do immediate delivery.

Here is a Jabber transport implementation that uses xmpp4r:
require 'xmpp4r/client'

module Merb
  module Messenger
    class Xmpp4rTransport
      include ::Jabber

      class_inheritable_accessor :_default_delivery_options
      self._default_delivery_options = Mash.new(:message_type => :chat)

      def self.delivery_options(options)
        self._default_delivery_options = options
      end

      attr_reader :transport

      def initialize(options)
        @transport = Client.new(options[:jid])
        @transport.connect
        
        @transport.auth(options[:password])
      end

      def connect
        self.transport.connect
      end

      def disconnect
        self.transport.close
      end

      def deliver(options = {})
        options = _default_delivery_options.merge(options)
        
        m = Message.new(options[:to], options[:body])
        
        m.set_subject(options[:subject]) if options[:subject]
        m.set_subject(options[:id])      if options[:id]
        m.set_type(options[:message_type] || :normal)
        
        self.transport.send(m)
      end
    end
  end
end


Setting transports up
=======================

Transports are usually set up before Merb loads your application
classes, but it's obviously neither a requirement, nor a convention.
It just makes messaging classes loaded before code that uses them loads.

To set up a transport you use
Merb::Messenger.setup_transport method that takes transport name
(can be anything, pick how you'd like to address that transport in your
application), transport type alias (:growl, :xmpp4r, can be anything people
already written transports for) and a Hash of options.

Since transports are so different by nature (Growl has host, port, password,
notifications list and application name options, XMPP usually has jid, server,
port, SASL/TLS options and password, smpt transport would have SMTP server
settings, sendmail would have a sendmail path, etc), possible options are
up to transport authors.

Merb::BootLoader.before_app_loads do
  Merb::Messenger.setup_transport(:lan_growl, :growl, {
    :application   => "merbpack",
    :notifications => ["journal_notification"]
  })
end

You can use transports directly if you need to. For instance,
you can set up a Jabber transport that's gonna notify you
when Merb worker is shut down or restarted. This way you
can be notified on crashes and application deployments.

Here is an example:

Merb::BootLoader.after_app_loads do
  im = Merb::MessagingController.message_transport :lan_growl

  im.deliver({
    :title        => "MerbPack presence",
    :body         => "MerbPack agent is back online",
    :notification => "presence notification"
  })
end


Merb::BootLoader.before_master_shutdown do
  Merb.logger.info "MerbPack agent is going offline..."
  im = Merb::MessagingController.message_transport :lan_growl

  im.deliver({
    :title        => "MerbPack presence",
    :body         => "MerbPack agent is about to shut down",
    :notification => "presence notification"
  })
end

Merb::BootLoader.before_worker_shutdown do
  Merb.logger.info "Shutting down worker..."

  Merb::MessagingController.message_transport(:remote).disconnect
end


Here we use two transports named :lan_growl and :remote. First uses
Growl transport in local area network, and second is a Jabber transport
that sends XMPP notifications.

We use them to notify people when application goes down and comes back
up online.

The Future
===================

Merb messenger will be part of Merb 1.1 in early 2009. The Holy Grails of
this plugin is to provide simple generic interface for messaging of all
kinds: from Jabber and mail notifications to talking to AMQP brokers
like RabbitMQ or ZeroMQ, doing "web hooks" and so forth.
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].