Skip to content

Instantly share code, notes, and snippets.

@M7
Created March 23, 2012 21:18
Show Gist options
  • Save M7/2175149 to your computer and use it in GitHub Desktop.
Save M7/2175149 to your computer and use it in GitHub Desktop.
Design Doc - Overview - Ruby Tuesday - Whenbot

In This Document

  1. General Overview
  2. High Level Concepts 1. Channels 2. Triggers 3. Actions 4. Tasks
  3. Technical Overview
  4. Whenbot Gem
  5. Channels 1. Service 2. Triggers 3. Actions

1. Overview

Whenbot is an open source clone of "If This, Then That" (ifttt.com) for a single user. Whenbot allows a user to create Triggers to be monitored for specific events, and Actions to occur once a Trigger fires.

For example, whenever you post an Instagram photo, create a Tumblr blog post.

1.2 High Level Concepts

At a high level, Whenbot can be broken down into four main concepts:

  1. Channels
  2. Triggers
  3. Actions
  4. Tasks

1.2.1. Channels

Channels are the web services that Whenbot can connect to, such as Gmail, Twitter, Instagram, and so on.

Each Channel can contain Triggers, or Actions, or both. Every Channel should have at least one Trigger or Action.

1.2.2. Triggers

Triggers are the events that Whenbot will watch for. For example, a Trigger can be set to watch for you to receive a new email.

Each Trigger is tied to a Channel. When a Trigger is "fired" (i.e. the conditions for a trigger are met), an Action is ready to be performed.

1.2.3. Actions

An Action is what Whenbot does once a Trigger is fired. For example, you could have an Action setup to create a new Tumblr post whenever you publish a new Instagram photo.

Actions are tied to Channels as well. Just like Triggers, you can't have an Action without a Channel.

1.2.4. Tasks

A Task is the term we use to describe the overall concept of a Trigger, the Trigger's settings (e.g. specific words to watch for), the Action that will be run when a Trigger is fired, and the Action's settings.

2. Technical Overview

In terms of the internal design, Whenbot has the following main components:

  1. Whenbot Gem
  2. Channels 1. Service 2. Triggers 3. Actions

Lets go a little deeper...

2.1 Component Descriptions

2.1.1. Whenbot Gem

This is is the control center of Whenbot. It houses the logic for creating new Tasks. It manages the Channels, Triggers and Actions, querying and invoking them as needed.

The Whenbot Gem is also responsible for relaying webhook data to the appropriate Trigger. It does this by registering itself to receive webhook results via the following route:

# routes.rb

match '/whenbot/:channel(/:trigger)/callback', to: 'whenbot#callback'

Whenever new callback data is received from a web service, the Gem will look at the parameters found in the URL, and send the data along to the appropriate Channel and, if given, a specific Trigger.

The Gem also manages the scheduling of the polling for all the Triggers, calling the appropriate Action once a saved Trigger has been matched, and the User Interface for creating Tasks as a whole.

2.1.2 Channels

Channels are the web services that Whenbot can connect to. This houses any Triggers and/or Actions belonging to the Channel, and the code to connect to and query the web service itself.

Here's a sample outline of the modules and classes that would be found in a Channel that has both a Trigger and an Action:

module Whenbot
  module GmailChannel  
  
    class Service
      # ...
    end
  
    module Triggers
      class NewEmailTrigger
        # ...
      end
    end
    
    module Actions
      class SendEmailAction
        # ...
      end
    end
  end
end

Channels are each developed separately. A Channel author may choose to develop a set of Triggers and/or Actions as well.

A User can install a new Channel in two steps:

  1. Add a Channel's gem to the Gemfile. For example,
``gem 'whenbot-gmail'``

(Remember to run ``bundle install`` afterwards.)
  1. Add the following to config/initializers/whenbot.rb:
```ruby
Whenbot.config do |config|
  config.add_channel Whenbot::GmailChannel
end
```

Once a Channel is registered with the Whenbot Gem, the Whenbot Gem can detect whether a Channel has any Triggers or Actions automagically.

2.1.3 Service

The Service class handles all of the web service related responsibilities. This includes connecting to the service, making appropriate calls to the API, setting up any webhooks, and any additional methods needed for other functionality (e.g. polling calls).

The Service class is contained in the Whenbot::<channel_name>Channel module:

module Whenbot
  module GmailChannel    
    class Service
      # Service methods
    end  
  end
end

The Service class must contain a method called callback:

def self.callback(data)
  # ...
end

Note that when registering for a webhook, or when polling your service, you must set the callback to /whenbot/<your_channel_name>/callback. The Whenbot Gem will catch these requests, and relay the webhook data to your Channel's Service.callback method

With this approach, your callback method will be responsible for determining which Trigger to call, based on the data received in the webhook.

If it's appropriate, however, it is recommended that you add the Trigger to the callback URL.

For instance, when polling or registering with Gmail to receive notification of any new emails that are received, the NewEmailTrigger in the GmailChannel would register its callback as follows:

/whenbot/gmail/new_email/callback

In this case, the GmailChannel::Triggers::NewEmailTrigger.callback method would receive the data from the webhook.

2.2.4 Triggers

N.B. Triggers can also be called "Channel Triggers," since each Trigger belongs to a specific Channel.

Once a Task has be saved by the User, a Trigger's most important job is to check any incoming callback data, as relayed by Whenbot, and call match_found if any of the saved Triggers came back as a match.

Triggers have a few methods that are called by the Whenbot Gem:

module Whenbot
  module GmailChannel  
    module Triggers
  
      class NewEmailFrom
        include Whenbot::Trigger

        # Technical Note:
        # We can replace the display_title, description and paramters
        # methods with something like:
        #     
        #   option :display_title, "New email from" 
        #   option :description, "Triggers whenever you receive a new email from "\
        #                        "a specified email address."        
        #
        # "option" would be a method that's automatically mixed-in to
        # this class. Thoughts?

        # Used by the UI to show the Triggers in a list
        def self.display_title
          "New email from"
        end
    
        # Additional description of this Trigger, shown in the UI
        def self.description
          "Triggers whenever you receive a new email from a specified email address."
        end     

        # A form will be automatically generated when the User is
        # creating a Task, to get the required parameters.
        # These parameters will be saved to the database.
        #
        # Returns: Hash of parameters to be obtained from the
        # user when setting up this Trigger.
        def self.parameters
          { 
            email_address: {
              label: 'Email',
              input_type: :text, # can also be :select, :checkbox, etc. 
              help_text: 'Email to watch for', # optional.
              optional: false # :optional is optional ;)
            }
          }
        end

        # Returns: true if this Trigger with the specified
        # params is a polling trigger. Otherwise, false.
        def self.is_polling_trigger?(params)
          # Check the given params hash and return 
          # true if this trigger can only be polled, and
          # false if it's possible to setup a webhook.
          # If false is returned, create_webhook_for(params)
          # will get called by the Gem.
        end

        # Optional. Only needed if this is a polling Trigger.
        #
        # Polls the web service for this Trigger, with
        # the given params. This method is called by 
        # Whenbot on regular intervals.
        def self.poll(params)
          # Note: When creating a webhook or polling a service, it is
          # important to set the callback URL to 
          # /webhooks/<channel_name>/<trigger_name>/callback
          # For example, this Trigger would use:
          # /webhooks/gmail/new_email/callback         
          #
          # Tip: If your service happens to be able to use the
          # resulting poll data for more than one Trigger, you
          # can set the callback path to your Service.callback
          # method, and have that method call each of your 
          # Triggers 
        end

        # Optional. Only needed if this is Trigger can be watched via a webhook.
        #
        # Creates a webhook on the server for this Trigger, with
        # the given params.
        #
        # Returns: id (string) => an identifier given by the webservice, 
        #   that is used to uniquely identify this hook.
        def self.create_webhook_for(params)
          # Note: When creating a webhook or polling a service, it is
          # important to set the callback URL to 
          # /webhooks/<channel_name>/<trigger_name>/callback
          # For example, this Trigger would use:
          # /webhooks/gmail/new_email/callback 
          # 
          # Tip: If your service happens to be able to use the
          # resulting webhook data for more than one Trigger, you
          # can set the callback path to your Service.callback
          # method, and have that method call each of your 
          # Triggers 
        end

        # Cancels a webhook that has been setup on the server
        # id => The unique id returned from create_webhook_for
        def self.cancel_webhook_for(id, params)
          # Optional. Only needed if this Trigger uses webhooks.
        end

        # Called by Whenbot whenever a new response is received 
        # from a web service for this Trigger.
        # body (string) => Result of request.body.read
        # headers (hash) => Result of request.headers
        # returns:  
        #   matches (array) => any matches found with the given inputs
        #   response_body (string) => (optional), useful if you need to
        #                            return a specific response to the 
        #                            web service.
        #   status (symbol or integer) => (optional) http status code
        def self.callback(params, body, headers)
          # 
        end
      
      end
    end
  end
end

2.2.5 Actions

Actions are the events that happen once a Trigger is fired.

The only required methods are self.parameters and perform. Here's an example Action class:

module Whenbot
  module TwitterChannel
  	module Actions
    
      class PostTweetAction

        option :display_image, "twitter.png"
        
        # Could also replace the methods below with something like:
        #
        # option :display_title, "Post a new Tweet"
        # option :description, "This Action will post a new tweet, with "\
        #                      "the text you specify"
        # 
        # parameter :message, { label: 'Tweet text', input_type: :text }

        def display_title
          "Post a new Tweet"
        end
        
        def description
          "This Action will post a new tweet, with the text you specify"
        end

     	  def parameters
     	    {
     	      message: 
     	      { 
     	        label: 'Tweet text', 
     	        input_type: :text 
     	      }
     	    } 
   	    end

        # Do the Action
        # params => This action's parameter data, as given by the user      
        # match_data => Data from the Trigger, for use by the Action
        def perform(params, match_data)
          # Connect to the webservice and perform the action
        end
      end
    end
  end
end

Components (diagrams)

Coming...

Task Creation Overview

From https://github.com/ottawaruby/whenbot/wiki/Whenbot-UI-app-outline

Task Creation Flow and Component Interactions

See: https://gist.github.com/2175095

Component Responsibilities

See: https://gist.github.com/2175087

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment