Skip to content
This repository has been archived by the owner on Mar 20, 2019. It is now read-only.

Whenbot Design Overview (New)

M7 edited this page May 4, 2012 · 8 revisions

In This Document

  1. General Overview
  2. High Level Concepts 1. Channels 2. Triggers 3. Actions 4. Tasks
  3. Technical Overview
  4. Whenbot App (core)
  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 App (core)
  2. Channels 1. Service 2. Triggers 3. Actions

Lets go a little deeper...

2.1 Component Descriptions

2.1.1. Whenbot App

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 App 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 App 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 App 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 App, the Whenbot App 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 TwitterChannel    
    class Service
      # Service methods
    end  
  end
end

The design of this class is mostly up to you. Its main purposes are:

  1. To provide an authentication parameters hash when requested, so that Whenbot can gather the credentials the service will need from the User (see below for an example).
  2. To connect to your chosen web service
  3. To support your Triggers or Actions (i.e. to make it easy to setup webhooks, poll the service, or perform any Actions.)
Required methods

The following outlines the methods that each Service class is expect to define:

module Whenbot
  module GmailChannel  

    class Service

      # Returns a hash of the autentication parameters that 
      # Whenbot should retrieve from the User. These credentials
      # will be stored in the database, and can be retreived
      # from the Trigger by calling #credentials. 
      def self.auth_parameters
        { 
          email_address: {
            label: 'Email',
            input_type: :text 
          }
          password: {
            label: 'Password'
            input_type: :text
          }
        }
        
        # With the above, you would retrieve the credentials back
        # with a call to #credentials. The :label and :input_type
        # data will be removed. For example:
        #
        #   login_info = credentials
        #   login_info[:email_address] # Value: user@gmail.com
        #   login_info[:password]      # Value: user_password
      end

      # (Optional) Called when a webhook is received for this Channel. 
      # Whenbot relays the params, body and headers as received from the
      # web service.
      # params (hash) => As given by the web service
      # 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 
      #                            service (e.g. a challenge response).
      #  status (symbol or integer) => (optional) http status code      
      def self.callback(params, body, headers)
        # 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 
        # /whenbot/<channel_name>/callback, and have that method 
        # call each of your Triggers.
        #
        # Bonus Tip: You can get a list of all Triggers in your
        # Channel dynamically with the #constants method.
        # e.g. GmailChannel::Triggers.constants  
        #       --> returns the array: [:NewEmailTrigger, :NewEmailFromTrigger]
      end    

    end
  end
end
The callback method

If you don't specify a Trigger in your callback URL (when registering for a webhook or sending a poll request), the only expectation of the Service class is that it have a #callback method.

For the example TwitterChannel below, you would set your callback to /whenbot/twitter/callback. The Whenbot App will then relay any received 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.

The recommended method, however, is to include the Trigger in the callback as well, as shown in the Triggers section below.

Getting started with your service

To get your Gem to work with a service, we suggest that you search RubyGems.org, Google or Github for a pre-existing gem. You'll find that most of the time, someone has taken the time to write a handy wrapper for the web service you're trying to use.

N.B. If you don't find a pre-made gem, this may be a good opportunity to give back to Open Source. ;)

Once you've found a gem and read the docs, add it to your gem's dependencies. For example:

s.add_runtime_dependency 'twitter', '~> 2.1.1'

From here, you'll want to wrap the functionality that your Trigger or Action requires by creating some new methods in your Service class.

Tip: Put shared functionality in your Service class

N.B. It may be tempting to call the methods directly from your Trigger's class, but if you'll be making more than one Trigger or Action, there's almost certainly going to shared functionality. That wouldn't be very DRYish. So, grouping your external API calls in the Service class is the recommended approach.

For example, say you wanted to be able to create an Action that would Post a new tweet. You could do the following:

module Whenbot
  module TwitterChannel 
    class Service
    
      # ....
      def self.post_tweet(message)
        Twitter.update(message)
      end   
      # ...
    
    end    
  end
end

And then, your Action would have:

module Whenbot
  module TwitterChannel
    module Actions
    
      # ....
      class PostTweetAction
        def self.perform(params)
          Service.post_tweet(params[:tweet])
        end
      end
      # ...

      # (Optional) Called when a webhook is received for this Channel. 
      # Whenbot relays the params, body and headers as received from the
      # web service.
      # params (hash) => As given by the web service
      # 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 
      #                            service (e.g. a challenge response).
      #  status (symbol or integer) => (optional) http status code      
      def self.callback(params, body, headers)
        # 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 
        # /whenbot/<channel_name>/callback, and have that method 
        # call each of your Triggers.
        #
        # Bonus Tip: You can get a list of all Triggers in your
        # Channel dynamically with the #constants method.
        # e.g. GmailChannel::Triggers.constants  
        #       --> returns the array: [:NewEmailTrigger, :NewEmailFromTrigger]
      end    
    
    end
  end
end
To sum up

So, the basic guidelines here are to:

  1. Define a method called auth_parameters that will return a hash of the authentication parameters that Whenbot should retrieve from the User.
  2. Group your external API calls in the Whenbot::YourChannel::Service class
  3. If you'll be setting your callbacks to /whenbot/your_channel/callback (instead of /whenbot/your_channel/your_trigger/callback), then: 1. Have a #callback method in your Service class

2.2.4 Triggers

N.B. You can call a Trigger a "Channel Trigger," if you'd like (in your mind ;), 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 App:

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 App.
        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 registering 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: The <trigger_name> portion of the above path 
          # is optional. So, 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 
          # /whenbot/<channel_name>/callback, 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

As mentioned in the above code, it is recommended that you include the Trigger name in 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 web service.

The Whenbot App will catch these requests, and relay the webhook data to your Trigger's callback method.

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

If it helps...

Just in case it helps, here's a few links to some more detailed information.

Whenbot Channel Directory Structure

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

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