-
Notifications
You must be signed in to change notification settings - Fork 54
Whenbot Design Overview (New)
- General Overview
- High Level Concepts 1. Channels 2. Triggers 3. Actions 4. Tasks
- Technical Overview
- Whenbot App (core)
- Channels 1. Service 2. Triggers 3. Actions
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.
At a high level, Whenbot can be broken down into four main concepts:
- Channels
- Triggers
- Actions
- Tasks
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.
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.
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.
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.
In terms of the internal design, Whenbot has the following main components:
- Whenbot App (core)
- Channels 1. Service 2. Triggers 3. Actions
Lets go a little deeper...
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.
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:
- Add a Channel's gem to the Gemfile. For example,
``gem 'whenbot-gmail'``
(Remember to run ``bundle install`` afterwards.)
- 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.
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:
- 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).
- To connect to your chosen web service
- To support your Triggers or Actions (i.e. to make it easy to setup webhooks, poll the service, or perform any Actions.)
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
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.
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.
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
So, the basic guidelines here are to:
- Define a method called
auth_parameters
that will return a hash of the authentication parameters that Whenbot should retrieve from the User. - Group your external API calls in the
Whenbot::YourChannel::Service
class - 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 yourService
class
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.
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
Just in case it helps, here's a few links to some more detailed information.
See: https://gist.github.com/2221561
From https://github.com/ottawaruby/whenbot/wiki/Whenbot-UI-app-outline