├── log
└── .gitkeep
├── lib
├── tasks
│ └── .gitkeep
├── assets
│ └── .gitkeep
├── whenbot
│ ├── version.rb
│ ├── channel.rb
│ ├── channels
│ │ ├── twitter
│ │ │ ├── service.rb
│ │ │ └── triggers
│ │ │ │ └── new_tweet_from.rb
│ │ └── developer
│ │ │ ├── service.rb
│ │ │ ├── actions
│ │ │ └── do_some_event.rb
│ │ │ └── triggers
│ │ │ └── sample_search.rb
│ └── trigger.rb
├── templates
│ └── erb
│ │ └── scaffold
│ │ └── _form.html.erb
└── whenbot.rb
├── public
├── favicon.ico
├── robots.txt
├── 500.html
├── 422.html
├── 404.html
└── index.html
├── test
├── unit
│ ├── .gitkeep
│ ├── helpers
│ │ ├── tasks_helper_test.rb
│ │ └── whenbot_helper_test.rb
│ ├── action_test.rb
│ ├── authentication_test.rb
│ ├── whenbot_test.rb
│ ├── task_test.rb
│ ├── triggers
│ │ ├── trigger_conformance_test.rb
│ │ └── sample_search_test.rb
│ └── trigger_test.rb
├── functional
│ ├── .gitkeep
│ ├── tasks_controller_test.rb
│ └── whenbot_controller_test.rb
├── integration
│ ├── .gitkeep
│ └── task_flows_test.rb
├── factories
│ ├── authentications.rb
│ ├── tasks.rb
│ ├── actions.rb
│ └── triggers.rb
├── performance
│ └── browsing_test.rb
└── test_helper.rb
├── app
├── mailers
│ └── .gitkeep
├── models
│ ├── .gitkeep
│ ├── authentication.rb
│ ├── action.rb
│ ├── task.rb
│ └── trigger.rb
├── helpers
│ ├── tasks_helper.rb
│ ├── whenbot_helper.rb
│ └── application_helper.rb
├── assets
│ ├── images
│ │ └── rails.png
│ ├── stylesheets
│ │ ├── tasks.css.scss
│ │ ├── whenbot.css.scss
│ │ ├── bootstrap_config.css.less
│ │ └── application.css
│ └── javascripts
│ │ ├── tasks.js.coffee
│ │ ├── whenbot.js.coffee
│ │ └── application.js
├── views
│ ├── tasks
│ │ ├── index.html.erb
│ │ ├── _channels.html.erb
│ │ └── new.html.erb
│ └── layouts
│ │ ├── _notice.html.erb
│ │ └── application.html.erb
└── controllers
│ ├── application_controller.rb
│ ├── tasks_controller.rb
│ └── whenbot_controller.rb
├── vendor
├── plugins
│ └── .gitkeep
└── assets
│ ├── javascripts
│ └── .gitkeep
│ └── stylesheets
│ └── .gitkeep
├── config.ru
├── config
├── environment.rb
├── boot.rb
├── initializers
│ ├── whenbot.rb
│ ├── mime_types.rb
│ ├── add_love.rb
│ ├── backtrace_silencers.rb
│ ├── session_store.rb
│ ├── secret_token.rb
│ ├── wrap_parameters.rb
│ ├── inflections.rb
│ ├── require_lib_files.rb
│ └── simple_form.rb
├── locales
│ ├── en.yml
│ └── simple_form.en.yml
├── environments
│ ├── development.rb
│ ├── test.rb
│ └── production.rb
├── database.yml.example
├── routes.rb
└── application.rb
├── doc
└── README_FOR_APP
├── Rakefile
├── script
└── rails
├── db
├── seeds.rb
├── migrate
│ ├── 20120419233318_create_authentications.rb
│ ├── 20120419234122_create_tasks.rb
│ ├── 20120419230901_create_actions.rb
│ └── 20120419221210_create_triggers.rb
└── schema.rb
├── .gitignore
├── LICENSE
├── Gemfile
├── .rvmrc
├── README
└── Gemfile.lock
/log/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/unit/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/mailers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/plugins/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/functional/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/integration/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/assets/javascripts/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/helpers/tasks_helper.rb:
--------------------------------------------------------------------------------
1 | module TasksHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/whenbot_helper.rb:
--------------------------------------------------------------------------------
1 | module WhenbotHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/lib/whenbot/version.rb:
--------------------------------------------------------------------------------
1 | module Whenbot
2 | VERSION = "0.0.1"
3 | end
4 |
--------------------------------------------------------------------------------
/lib/whenbot/channel.rb:
--------------------------------------------------------------------------------
1 | module Whenbot
2 | module Channel
3 |
4 | end
5 | end
--------------------------------------------------------------------------------
/app/assets/images/rails.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ottawaruby/whenbot/HEAD/app/assets/images/rails.png
--------------------------------------------------------------------------------
/app/views/tasks/index.html.erb:
--------------------------------------------------------------------------------
1 |
Tasks#index
2 | Find me in app/views/tasks/index.html.erb
3 |
--------------------------------------------------------------------------------
/test/unit/helpers/tasks_helper_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class TasksHelperTest < ActionView::TestCase
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery
3 | end
4 |
--------------------------------------------------------------------------------
/test/unit/helpers/whenbot_helper_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class WhenbotHelperTest < ActionView::TestCase
4 | end
5 |
--------------------------------------------------------------------------------
/app/views/tasks/_channels.html.erb:
--------------------------------------------------------------------------------
1 | <% channels.each do |channel| %>
2 |
3 |
<%= channel %>
4 |
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/app/models/authentication.rb:
--------------------------------------------------------------------------------
1 | class Authentication < ActiveRecord::Base
2 | serialize :parameters, Hash
3 | attr_accessible :channel, :parameters
4 | end
5 |
--------------------------------------------------------------------------------
/app/views/layouts/_notice.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
x
3 | <%= flash[:notice] %>
4 |
--------------------------------------------------------------------------------
/test/unit/action_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ActionTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Whenbot::Application
5 |
--------------------------------------------------------------------------------
/app/views/tasks/new.html.erb:
--------------------------------------------------------------------------------
1 | Create New Task
2 |
3 |
Trigger Channels
4 | <%= render 'channels', channels: @channels %>
5 |
6 |
--------------------------------------------------------------------------------
/test/unit/authentication_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class AuthenticationTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | Whenbot::Application.initialize!
6 |
--------------------------------------------------------------------------------
/app/controllers/tasks_controller.rb:
--------------------------------------------------------------------------------
1 | class TasksController < ApplicationController
2 | def index
3 | end
4 |
5 | def new
6 | @channels = Whenbot.trigger_channels
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/whenbot/channels/twitter/service.rb:
--------------------------------------------------------------------------------
1 | module Whenbot
2 | module Channels
3 | module Twitter
4 | class Service
5 |
6 | # TODO
7 |
8 | end
9 | end
10 | end
11 | end
--------------------------------------------------------------------------------
/app/assets/stylesheets/tasks.css.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the tasks controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 |
--------------------------------------------------------------------------------
/app/models/action.rb:
--------------------------------------------------------------------------------
1 | class Action < ActiveRecord::Base
2 | belongs_to :task
3 |
4 | serialize :parameters, Hash
5 | serialize :extra_data, Hash
6 | attr_accessible :channel, :action, :parameters, :active
7 | end
8 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/whenbot.css.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the Whenbot controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 |
3 | # Set up gems listed in the Gemfile.
4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
5 |
6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
7 |
--------------------------------------------------------------------------------
/config/initializers/whenbot.rb:
--------------------------------------------------------------------------------
1 | require 'whenbot'
2 |
3 | # Add your installed Channels here.
4 | Whenbot.config do |config|
5 | config.channels << Whenbot::Channels::Developer
6 | config.channels << Whenbot::Channels::Twitter
7 | end
--------------------------------------------------------------------------------
/doc/README_FOR_APP:
--------------------------------------------------------------------------------
1 | Use this README file to introduce your application and point to useful places in the API for learning more.
2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries.
3 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-Agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/test/factories/authentications.rb:
--------------------------------------------------------------------------------
1 | # Read about factories at https://github.com/thoughtbot/factory_girl
2 |
3 | FactoryGirl.define do
4 | factory :authentication do
5 | channel "MyString"
6 | parameters "MyText"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | hello: "Hello world"
6 |
--------------------------------------------------------------------------------
/app/assets/javascripts/tasks.js.coffee:
--------------------------------------------------------------------------------
1 | # Place all the behaviors and hooks related to the matching controller here.
2 | # All this logic will automatically be available in application.js.
3 | # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
4 |
--------------------------------------------------------------------------------
/app/assets/javascripts/whenbot.js.coffee:
--------------------------------------------------------------------------------
1 | # Place all the behaviors and hooks related to the matching controller here.
2 | # All this logic will automatically be available in application.js.
3 | # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
4 |
--------------------------------------------------------------------------------
/config/initializers/add_love.rb:
--------------------------------------------------------------------------------
1 | class AddLove
2 | def initialize(app)
3 | @app = app
4 | end
5 |
6 | def call(env)
7 | status, headers, response = @app.call(env)
8 | headers["X-Who-Loves-You"] = 'whenbot'
9 | [status, headers, response.body]
10 | end
11 | end
--------------------------------------------------------------------------------
/app/assets/stylesheets/bootstrap_config.css.less:
--------------------------------------------------------------------------------
1 | @import "twitter/bootstrap";
2 | @import "twitter/bootstrap/responsive.less";
3 |
4 | .simple_form {
5 | .form-horizontal;
6 |
7 | .form-actions {
8 | input[type=submit] {
9 | .btn-primary;
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | # Add your own tasks in files placed in lib/tasks ending in .rake,
3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
4 |
5 | require File.expand_path('../config/application', __FILE__)
6 |
7 | Whenbot::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/lib/whenbot/channels/developer/service.rb:
--------------------------------------------------------------------------------
1 | module Whenbot
2 | module Channels
3 | module Developer
4 | class Service
5 |
6 | #
7 | # Place common code here to handle calls to
8 | # your webservice.
9 | #
10 |
11 | end
12 | end
13 | end
14 | end
--------------------------------------------------------------------------------
/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 |
4 | APP_PATH = File.expand_path('../../config/application', __FILE__)
5 | require File.expand_path('../../config/boot', __FILE__)
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/test/functional/tasks_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class TasksControllerTest < ActionController::TestCase
4 | test "should get index" do
5 | get :index
6 | assert_response :success
7 | end
8 |
9 | test "should get new" do
10 | get :new
11 | assert_response :success
12 | end
13 |
14 | end
15 |
--------------------------------------------------------------------------------
/test/factories/tasks.rb:
--------------------------------------------------------------------------------
1 | # Read about factories at https://github.com/thoughtbot/factory_girl
2 |
3 | FactoryGirl.define do
4 | factory :task do
5 | name "My Task"
6 | active true
7 | last_executed "2012-04-19 19:41:22"
8 |
9 | factory :developer_task do
10 | name "Developer Task"
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/factories/actions.rb:
--------------------------------------------------------------------------------
1 | # Read about factories at https://github.com/thoughtbot/factory_girl
2 |
3 | FactoryGirl.define do
4 | factory :action do
5 | task_id 1
6 | channel "MyString"
7 | action "MyString"
8 | parameters "MyText"
9 | last_executed "2012-04-19 19:09:01"
10 | extra_data "MyText"
11 | active true
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
7 | # Mayor.create(name: 'Emanuel', city: cities.first)
8 |
--------------------------------------------------------------------------------
/db/migrate/20120419233318_create_authentications.rb:
--------------------------------------------------------------------------------
1 | class CreateAuthentications < ActiveRecord::Migration
2 | def change
3 | create_table :authentications do |t|
4 | t.string :channel, null: false
5 | t.text :parameters
6 |
7 | t.timestamps
8 | end
9 |
10 | add_index :authentications, :channel, unique: true
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/db/migrate/20120419234122_create_tasks.rb:
--------------------------------------------------------------------------------
1 | class CreateTasks < ActiveRecord::Migration
2 | def change
3 | create_table :tasks do |t|
4 | t.string :name
5 | t.datetime :last_executed
6 | t.boolean :active, null: false, default: true
7 |
8 | t.timestamps
9 | end
10 |
11 | add_index :tasks, :active
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/whenbot/trigger.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/class/attribute"
2 |
3 | module Whenbot
4 | module Trigger
5 |
6 | def self.included(base)
7 | base.extend ClassMethods
8 | end
9 |
10 | module ClassMethods
11 |
12 | # Use this when setting :last_matched value
13 | def current_time
14 | Time.zone.now
15 | end
16 |
17 | end
18 | end
19 | end
--------------------------------------------------------------------------------
/lib/templates/erb/scaffold/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%%= simple_form_for(@<%= singular_table_name %>) do |f| %>
2 | <%%= f.error_notification %>
3 |
4 |
5 | <%- attributes.each do |attribute| -%>
6 | <%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %>
7 | <%- end -%>
8 |
9 |
10 |
11 | <%%= f.button :submit %>
12 |
13 | <%% end %>
14 |
--------------------------------------------------------------------------------
/test/performance/browsing_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'rails/performance_test_help'
3 |
4 | class BrowsingTest < ActionDispatch::PerformanceTest
5 | # Refer to the documentation for all available options
6 | # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory]
7 | # :output => 'tmp/performance', :formats => [:flat] }
8 |
9 | def test_homepage
10 | get '/'
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Whenbot::Application.config.session_store :cookie_store, key: '_whenbot_session'
4 |
5 | # Use the database for sessions instead of the cookie-based default,
6 | # which shouldn't be used to store highly confidential information
7 | # (create the session table with "rails generate session_migration")
8 | # Whenbot::Application.config.session_store :active_record_store
9 |
--------------------------------------------------------------------------------
/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 | Whenbot::Application.config.secret_token = '58346e466b61a5d0de4bce286af184ec95f0b429755c754ff6a6cc9536955bbe9c0278c30493630e70ad0c96b0ef94608ebf48760b9fd62a2ae89ecf9b576372'
8 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # Disable root element in JSON by default.
12 | ActiveSupport.on_load(:active_record) do
13 | self.include_root_in_json = false
14 | end
15 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format
4 | # (all these examples are active by default):
5 | # ActiveSupport::Inflector.inflections do |inflect|
6 | # inflect.plural /^(ox)$/i, '\1en'
7 | # inflect.singular /^(ox)en/i, '\1'
8 | # inflect.irregular 'person', 'people'
9 | # inflect.uncountable %w( fish sheep )
10 | # end
11 | #
12 | # These inflection rules are supported but not enabled by default:
13 | # ActiveSupport::Inflector.inflections do |inflect|
14 | # inflect.acronym 'RESTful'
15 | # end
16 |
--------------------------------------------------------------------------------
/db/migrate/20120419230901_create_actions.rb:
--------------------------------------------------------------------------------
1 | class CreateActions < ActiveRecord::Migration
2 | def change
3 | create_table :actions do |t|
4 | t.integer :task_id, null: false
5 | t.string :channel, null: false
6 | t.string :action, null: false
7 | t.text :parameters
8 | t.text :extra_data
9 | t.boolean :active, null: false, default: true
10 |
11 | t.timestamps
12 | end
13 |
14 | add_index :actions, :task_id
15 | add_index :actions, :active
16 | add_index :actions, [:channel, :action]
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/whenbot/channels/developer/actions/do_some_event.rb:
--------------------------------------------------------------------------------
1 | module Whenbot
2 | module Channels
3 | module Developer
4 | module Actions
5 |
6 | class DoSomeEvent
7 |
8 | # Task: Enable this functionality. See tests in trigger_test.rb
9 | #
10 | # set_option :display_title, 'SampleSearch trigger'
11 | # set_option :description, 'Does nothing. Always reports a match.'
12 |
13 | #
14 | # Perform the Action
15 | #
16 | def self.perform(parameters)
17 | # ...
18 | end
19 |
20 | end
21 |
22 | end
23 | end
24 | end
25 | end
--------------------------------------------------------------------------------
/config/initializers/require_lib_files.rb:
--------------------------------------------------------------------------------
1 | # Rails doesn't autoload files that contain classes or modules that
2 | # have already been defined (i.e. when you try and re-open a class
3 | # or module). We'll be doing a bunch of that here, so we'll
4 | # manually require the files in the lib folder, along with
5 | # its subfolders.
6 | #
7 | # This is also required to load the Triggers and Actions for
8 | # automatic detection of Triggers/Actions after a Channel is
9 | # added.
10 |
11 | # Solution from here:
12 | # - http://stackoverflow.com/a/3181988/321896
13 | # and
14 | # - http://stackoverflow.com/a/6797707/321896
15 |
16 | Dir[Rails.root + 'lib/**/*.rb'].each do |file|
17 | require file
18 | end
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile ~/.gitignore_global
6 |
7 | # Ignore bundler config
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 |
13 | # Ignore all logfiles and tempfiles.
14 | /log/*.log
15 | /tmp
16 |
17 | # This code will be Open Source, so don't include files with sensitive data
18 | /config/database.yml
19 |
20 | # Ignore .watchr file
21 | /.watchr
22 |
23 | # Random files to ignore
24 | /lib/whenbot/old_trigger.rb
25 |
26 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the top of the
9 | * compiled file, but it's generally better to create a new file per style scope.
10 | *
11 | *= require twitter/bootstrap
12 | *= require_self
13 | *= require_tree .
14 | */
15 |
16 | body {
17 | padding-top: 40px;
18 | padding-bottom: 40px;
19 | }
--------------------------------------------------------------------------------
/app/models/task.rb:
--------------------------------------------------------------------------------
1 | class Task < ActiveRecord::Base
2 | has_many :triggers
3 | # ==== One-liner 19 ====
4 |
5 | accepts_nested_attributes_for :triggers, :actions
6 |
7 | # ==== One-liner 20 ====
8 |
9 | #
10 | # Handles calling the appropriate Trigger's #callback method,
11 | # saves the match data to the table, and runs the Action(s) if
12 | # its conditions are met.
13 | #
14 | def self.handle_callback(channel, trigger, params, headers, body)
15 | triggers, trigger_ids = Trigger.triggers_for(channel, trigger)
16 | returned_triggers, response = Whenbot.relay_callback(channel, trigger, triggers, params, headers, body)
17 | # ==== One-liner 21 ====
18 | # ==== One-liner 22 ====
19 | end
20 |
21 | end
22 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
We're sorry, but something went wrong.
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/integration/task_flows_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class TaskFlowsTest < ActionDispatch::IntegrationTest
4 |
5 | test "New Task page should show installed Trigger Channels" do
6 | visit tasks_new_path
7 | assert page.has_text?('Trigger Channels'), "Trigger Channels list was not found"
8 | assert page.has_text?('Developer'), "Developer Channel was not detected"
9 | end
10 |
11 | test "New Task page should show Triggers when a Channel is clicked" do
12 | visit tasks_new_path
13 | assert page.has_no_text?('Sample Search'), "Sample Search trigger is shown before User clicked on the \"Developer\" Channel link"
14 | click_on 'Developer'
15 | assert page.has_text?('Sample Search'), "Developer's Sample Search Trigger wasn't displayed"
16 | end
17 |
18 | end
19 |
--------------------------------------------------------------------------------
/config/locales/simple_form.en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | simple_form:
3 | "yes": 'Yes'
4 | "no": 'No'
5 | required:
6 | text: 'required'
7 | mark: '*'
8 | # You can uncomment the line below if you need to overwrite the whole required html.
9 | # When using html, text and mark won't be used.
10 | # html: '*'
11 | error_notification:
12 | default_message: "Some errors were found, please take a look:"
13 | # Labels and hints examples
14 | # labels:
15 | # password: 'Password'
16 | # user:
17 | # new:
18 | # email: 'E-mail para efetuar o sign in.'
19 | # edit:
20 | # email: 'E-mail.'
21 | # hints:
22 | # username: 'User name to sign in.'
23 | # password: 'No special characters, please.'
24 |
25 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The change you wanted was rejected.
23 |
Maybe you tried to change something you didn't have access to.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The page you were looking for doesn't exist.
23 |
You may have mistyped the address or the page may have moved.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // the compiled file.
9 | //
10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
11 | // GO AFTER THE REQUIRES BELOW.
12 | //
13 | //= require jquery
14 | //= require jquery_ujs
15 | //= require_tree .
16 | //= require twitter/bootstrap
17 |
18 | // Initialize Twitter Bootstrap dropdowns
19 | $('.dropdown-toggle').dropdown()
20 |
--------------------------------------------------------------------------------
/app/controllers/whenbot_controller.rb:
--------------------------------------------------------------------------------
1 | class WhenbotController < ApplicationController
2 |
3 | # /whenbot/:channel/:trigger/callback
4 | def callback
5 |
6 | # ==== One-liner 13 ====
7 | response = validate_response response
8 |
9 | if response[:head_only]
10 | head response[:status] # ==== One-liner 15 ====
11 | else
12 | render response[:type] => response[:body],
13 | :status => response[:status],
14 | :layout => false
15 | end
16 | end
17 |
18 | private
19 |
20 | def validate_response(response)
21 | response ||= {}
22 | response[:head_only] ||= response[:body] ? false : true
23 | response[:status] ||= :ok
24 | response[:type] ||= :json
25 | response[:headers] ||= ''
26 | response[:body] ||= 'Success'
27 | response
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/db/migrate/20120419221210_create_triggers.rb:
--------------------------------------------------------------------------------
1 | class CreateTriggers < ActiveRecord::Migration
2 | def change
3 | create_table :triggers do |t|
4 | t.integer :task_id, null: false
5 | t.string :channel, null: false
6 | t.string :trigger, null: false
7 | t.boolean :polling_trigger, null: false, default: false
8 | t.text :parameters
9 | t.text :match_data
10 | t.datetime :last_matched
11 | t.text :extra_data
12 | t.string :webhook_uid
13 | t.boolean :active, null: false, default: true
14 |
15 | t.timestamps
16 | end
17 |
18 | add_index :triggers, :task_id
19 | add_index :triggers, :polling_trigger
20 | add_index :triggers, :active
21 | add_index :triggers, [:active, :polling_trigger]
22 | add_index :triggers, [:channel, :trigger]
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/factories/triggers.rb:
--------------------------------------------------------------------------------
1 | # Read about factories at https://github.com/thoughtbot/factory_girl
2 |
3 | FactoryGirl.define do
4 | factory :trigger do
5 | task_id { |t| t.association(:task) }
6 | channel "Developer"
7 | trigger "SampleSearch"
8 | polling_trigger false
9 | parameters({ search_term: 'MyText' })
10 | match_data nil
11 | extra_data nil
12 | last_matched "2012-04-19 19:41:22"
13 | webhook_uid "MyString"
14 | active true
15 |
16 | factory :matching_trigger do
17 | parameters({ search_term: 'banana' })
18 | extra_data({ some_value: 'saved result' })
19 | end
20 |
21 | factory :non_matching_trigger do
22 | parameters({ search_term: "a string that shouldn't be matched" })
23 | end
24 |
25 | factory :twitter_new_tweet_from_trigger do
26 | channel "Twitter"
27 | trigger "NewTweetFrom"
28 | parameters({ username: 'rails' })
29 | task_id { |task| task.association(:developer_task) }
30 | end
31 |
32 | factory :single_trigger_under_new_task do
33 | task
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2015 John Tajima and Ottawa Ruby.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/test/unit/whenbot_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class WhenbotTest < ActiveSupport::TestCase
4 |
5 |
6 |
7 | # test 'can return a list of Trigger Channels' do
8 | # channels = Whenbot.trigger_channels
9 | # assert_not_nil channels, "No channels were returned"
10 | # assert channels.is_a? Enumerable, "Did not return an enumerable list"
11 | # end
12 | #
13 | # test "including a Trigger adds it to the trigger_channels list" do
14 | # Whenbot.channels = nil
15 | # assert Whenbot.channels.empty?, "Could not clear Whenbot.channels list"
16 | # puts "Whenbot.trigger_channels = #{Whenbot.trigger_channels.inspect}"
17 | # assert Whenbot.trigger_channels.empty?, "trigger_channels list should be empty"
18 | #
19 | # klass = Whenbot::Channels::Twitter::Triggers::NewTweetFrom
20 | # Whenbot::Trigger.send :included, klass
21 | # trigger_channels = Whenbot.trigger_channels
22 | # assert Whenbot.trigger_channels.include? klass, "trigger_channels should include #{klass}"
23 | # end
24 | #
25 | # test 'including the Trigger module adds the trigger to trigger_channels' do
26 | # # ... write a better test?
27 | # end
28 | end
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] = "test"
2 | require File.expand_path('../../config/environment', __FILE__)
3 | require 'rails/test_help'
4 | require 'whenbot'
5 | require 'capybara/rails'
6 | require 'factory_girl_rails'
7 |
8 | class ActiveSupport::TestCase
9 |
10 | # fixtures :all # <-- Use the factories instead
11 |
12 | def raw_post(action, params, body)
13 | @request.env['RAW_POST_DATA'] = body
14 | response = post(action, params)
15 | @request.env.delete('RAW_POST_DATA')
16 | response
17 | end
18 |
19 | # Capybara Settings
20 | #
21 | # Transactional fixtures do not work with Selenium tests, because Capybara
22 | # uses a separate server thread, which the transactions would be hidden
23 | # from. We hence use DatabaseCleaner to truncate our test database.
24 | DatabaseCleaner.strategy = :truncation
25 |
26 | class ActionDispatch::IntegrationTest
27 | # Make the Capybara DSL available in all integration tests
28 | include Capybara::DSL
29 |
30 | # Stop ActiveRecord from wrapping tests in transactions
31 | self.use_transactional_fixtures = false
32 |
33 | teardown do
34 | DatabaseCleaner.clean # Truncate the database
35 | Capybara.reset_sessions! # Forget the (simulated) browser state
36 | Capybara.use_default_driver # Revert Capybara.current_driver to Capybara.default_driver
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Whenbot::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Log error messages when you accidentally call methods on nil.
10 | config.whiny_nils = true
11 |
12 | # Show full error reports and disable caching
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger
20 | config.active_support.deprecation = :log
21 |
22 | # Only use best-standards-support built into browsers
23 | config.action_dispatch.best_standards_support = :builtin
24 |
25 | # Raise exception on mass assignment protection for Active Record models
26 | config.active_record.mass_assignment_sanitizer = :strict
27 |
28 | # Log the query plan for queries taking more than this (works
29 | # with SQLite, MySQL, and PostgreSQL)
30 | config.active_record.auto_explain_threshold_in_seconds = 0.5
31 |
32 | # Do not compress assets
33 | config.assets.compress = false
34 |
35 | # Expands the lines which load the assets
36 | config.assets.debug = true
37 | end
38 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'http://rubygems.org'
2 |
3 | gem 'rails', '3.2.3'
4 |
5 | # Bundle edge Rails instead:
6 | # gem 'rails', :git => 'git://github.com/rails/rails.git'
7 |
8 | gem 'pg'
9 |
10 |
11 | # Gems used only for assets and not required
12 | # in production environments by default.
13 | group :assets do
14 | gem 'sass-rails', '~> 3.2.3'
15 | gem 'coffee-rails', '~> 3.2.1'
16 |
17 | gem 'less-rails-bootstrap'
18 |
19 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes
20 | gem 'therubyracer', :platform => :ruby
21 |
22 | gem 'uglifier', '>= 1.0.3'
23 | end
24 |
25 | gem 'jquery-rails'
26 | gem 'simple_form'
27 |
28 | gem 'heroku'
29 |
30 | group :test, :development do
31 | gem 'awesome_print'
32 | gem 'factory_girl_rails'
33 | end
34 |
35 | group :development do
36 | #gem 'ruby-debug19', :require => 'ruby-debug' # To use debugger
37 | gem 'rails-footnotes', '>= 3.7'
38 | end
39 |
40 | group :test do
41 | gem 'test-unit' # not bundled with ruby 1.9
42 | gem 'capybara', :git => 'git://github.com/jnicklas/capybara.git' # For #has_text?
43 | gem 'database_cleaner' # For Capybara. See test_helper.rb for details
44 | gem 'mocha', :require => false
45 | gem 'shoulda'
46 | end
47 |
48 | # To use ActiveModel has_secure_password
49 | # gem 'bcrypt-ruby', '~> 3.0.0'
50 |
51 | # To use Jbuilder templates for JSON
52 | # gem 'jbuilder'
53 |
54 | # Use unicorn as the app server
55 | # gem 'unicorn'
56 |
57 | # Deploy with Capistrano
58 | # gem 'capistrano'
59 |
60 | # To use debugger
61 | # gem 'ruby-debug19', :require => 'ruby-debug'
62 |
--------------------------------------------------------------------------------
/test/functional/whenbot_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class WhenbotControllerTest < ActionController::TestCase
4 |
5 | context "webhook callback access" do
6 |
7 | setup do
8 | @sample_search_trigger = Whenbot::Channels::Developer::Triggers::SampleSearch
9 | @matching = FactoryGirl.create(:matching_trigger)
10 | @non_matching = FactoryGirl.create(:non_matching_trigger)
11 | @search_term = @matching[:parameters][:search_term]
12 |
13 | @body_as_hash = { content: "Some text that mentions #{@search_term}" }
14 | @body = @body_as_hash.to_json
15 | @url_params = { url_data: "some data" }
16 | @headers = { 'Content-Type' => 'application/json' }
17 | end
18 |
19 | should "route paths with Channel and Trigger to Whenbot#callback" do
20 | expected_params = { controller: 'whenbot',
21 | action: 'callback',
22 | channel: 'developer',
23 | trigger: 'sample_search' }
24 | assert_routing '/whenbot/developer/sample_search/callback', expected_params
25 | end
26 |
27 | should "relay received webhook data to appropriate Trigger's #callback method" do
28 | triggers, ids = Trigger.triggers_for('Developer', 'SampleSearch')
29 | @sample_search_trigger.expects(:callback).returns([triggers, {status: :ok}])
30 |
31 | # Task for reader (that's you): Use Fakeweb or VCR.
32 | params = { channel: 'developer', trigger: 'sample_search'}
33 | response = raw_post('callback', params, @body)
34 | end
35 |
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Whenbot::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Configure static asset server for tests with Cache-Control for performance
11 | config.serve_static_assets = true
12 | config.static_cache_control = "public, max-age=3600"
13 |
14 | # Log error messages when you accidentally call methods on nil
15 | config.whiny_nils = true
16 |
17 | # Show full error reports and disable caching
18 | config.consider_all_requests_local = true
19 | config.action_controller.perform_caching = false
20 |
21 | # Raise exceptions instead of rendering exception templates
22 | config.action_dispatch.show_exceptions = false
23 |
24 | # Disable request forgery protection in test environment
25 | config.action_controller.allow_forgery_protection = false
26 |
27 | # Tell Action Mailer not to deliver emails to the real world.
28 | # The :test delivery method accumulates sent emails in the
29 | # ActionMailer::Base.deliveries array.
30 | config.action_mailer.delivery_method = :test
31 |
32 | # Raise exception on mass assignment protection for Active Record models
33 | config.active_record.mass_assignment_sanitizer = :strict
34 |
35 | # Print deprecation notices to the stderr
36 | config.active_support.deprecation = :stderr
37 | end
38 |
--------------------------------------------------------------------------------
/.rvmrc:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This is an RVM Project .rvmrc file, used to automatically load the ruby
4 | # development environment upon cd'ing into the directory
5 |
6 | # First we specify our desired [@], the @gemset name is optional.
7 | environment_id="ruby-1.9.3-p125@whenbot"
8 |
9 | #
10 | # Uncomment following line if you want options to be set only for given project.
11 | #
12 | # PROJECT_JRUBY_OPTS=( --1.9 )
13 |
14 | #
15 | # First we attempt to load the desired environment directly from the environment
16 | # file. This is very fast and efficient compared to running through the entire
17 | # CLI and selector. If you want feedback on which environment was used then
18 | # insert the word 'use' after --create as this triggers verbose mode.
19 | #
20 | if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \
21 | && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
22 | then
23 | \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
24 |
25 | if [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]]
26 | then
27 | . "${rvm_path:-$HOME/.rvm}/hooks/after_use"
28 | fi
29 | else
30 | # If the environment file has not yet been created, use the RVM CLI to select.
31 | if ! rvm --create "$environment_id"
32 | then
33 | echo "Failed to create RVM environment '${environment_id}'."
34 | exit 1
35 | fi
36 | fi
37 |
38 | #
39 | # If you use an RVM gemset file to install a list of gems (*.gems), you can have
40 | # it be automatically loaded. Uncomment the following and adjust the filename if
41 | # necessary.
42 | #
43 | # filename=".gems"
44 | # if [[ -s "$filename" ]]
45 | # then
46 | # rvm gemset import "$filename" | grep -v already | grep -v listed | grep -v complete | sed '/^$/d'
47 | # fi
48 |
49 | # If you use bundler, this might be useful to you:
50 | # if command -v bundle && [[ -s Gemfile ]]
51 | # then
52 | # bundle install
53 | # fi
54 |
55 |
56 |
--------------------------------------------------------------------------------
/config/database.yml.example:
--------------------------------------------------------------------------------
1 | # Note: For local development and testing, you'll need a database.yml file.
2 | # Rename this file to database.yml, and update the information as appropriate
3 | # For Heroku, you won't need the database.yml.
4 |
5 |
6 | # PostgreSQL. Versions 8.2 and up are supported.
7 | #
8 | # Install the pg driver:
9 | # gem install pg
10 | # On Mac OS X with macports:
11 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
12 | # On Windows:
13 | # gem install pg
14 | # Choose the win32 build.
15 | # Install PostgreSQL and put its /bin directory on your path.
16 | #
17 | # Configure Using Gemfile
18 | # gem 'pg'
19 | #
20 | development:
21 | adapter: postgresql
22 | encoding: unicode
23 | database: whenbot_development
24 | pool: 5
25 | username: whenbot
26 | password:
27 |
28 | # Connect on a TCP socket. Omitted by default since the client uses a
29 | # domain socket that doesn't need configuration. Windows does not have
30 | # domain sockets, so uncomment these lines.
31 | #host: localhost
32 | #port: 5432
33 |
34 | # Schema search path. The server defaults to $user,public
35 | #schema_search_path: myapp,sharedapp,public
36 |
37 | # Minimum log levels, in increasing order:
38 | # debug5, debug4, debug3, debug2, debug1,
39 | # log, notice, warning, error, fatal, and panic
40 | # The server defaults to notice.
41 | #min_messages: warning
42 |
43 | # Warning: The database defined as "test" will be erased and
44 | # re-generated from your development database when you run "rake".
45 | # Do not set this db to the same as development or production.
46 | test:
47 | adapter: postgresql
48 | encoding: unicode
49 | database: whenbot_test
50 | pool: 5
51 | username: whenbot
52 | password:
53 |
54 | production:
55 | adapter: postgresql
56 | encoding: unicode
57 | database: whenbot_production
58 | pool: 5
59 | username: whenbot
60 | password:
61 |
--------------------------------------------------------------------------------
/test/unit/task_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class TaskTest < ActiveSupport::TestCase
4 |
5 | context "handle_callback method" do
6 |
7 | setup do
8 | @channel = 'Developer'
9 | @trigger = 'SampleSearch'
10 |
11 | search_term = FactoryGirl.build(:matching_trigger).parameters[:search_term]
12 | @body_as_hash = { content: "Some text that mentions #{search_term}" }
13 | @body = @body_as_hash.to_json
14 | @url_params = { url_data: "some data" }
15 | @headers = { 'Content-Type' => 'application/json' }
16 | end
17 |
18 | should "call Whenbot#relay_callback to send the callback to the Trigger" do
19 | triggers, ids = Trigger.triggers_for('Developer', 'SampleSearch')
20 | Whenbot.expects(:relay_callback).returns([triggers, {status: :ok}])
21 | Task.handle_callback(@channel, @trigger, @url_params, @headers, @body)
22 | end
23 |
24 | should "update the match_data column for the matching data" do
25 | matching = FactoryGirl.create(:matching_trigger)
26 | non_matching = FactoryGirl.create(:non_matching_trigger)
27 | triggers, trigger_ids = Trigger.triggers_for(@channel, @trigger)
28 |
29 | Task.handle_callback(@channel, @trigger, @url_params, @headers, @body)
30 |
31 | # The match_data field is a serialized hash, and is stored as YAML
32 | # in the database, so we have to convert it back before the test.
33 | match_data = Trigger.find(matching.id).match_data
34 | assert_equal @body_as_hash[:content], match_data[:content], "Body content wasn't saved"
35 | end
36 |
37 | should "return response" do
38 | response = Task.handle_callback(@channel, @trigger, nil, nil, nil)
39 | assert_kind_of Hash, response, "response was not a hash"
40 | assert response.has_key?(:status), "Did not have a status key"
41 | # Other keys are optional.
42 | end
43 |
44 |
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Whenbot
6 |
7 | <%= stylesheet_link_tag "application", :media => "all" %>
8 | <%= javascript_include_tag "application" %>
9 | <%= csrf_meta_tags %>
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
Whenbot
25 |
26 |
27 |
28 |
29 |
30 | - <%= link_to 'Tasks', tasks_path %>
31 |
32 |
33 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | <%= render 'layouts/notice' unless notice.blank? %>
49 | <%= yield %>
50 |
51 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/lib/whenbot/channels/twitter/triggers/new_tweet_from.rb:
--------------------------------------------------------------------------------
1 | module Whenbot
2 | module Channels
3 | module Twitter
4 | module Triggers
5 | class NewTweetFrom
6 |
7 | include Whenbot::Trigger
8 |
9 | # Task: Enable this functionality. See tests in trigger_test.rb
10 | #
11 | # set_option :display_title, "New tweet posted by a user"
12 | # set_option :description, "Triggers whenever a new message is tweeted by "\
13 | # "a specific user."
14 |
15 | # Called by Whenbot whenever a new response is received
16 | # from a web service for this Trigger.
17 | # body (string) => Result of request.body.read
18 | # headers (hash) => Result of request.headers
19 | def self.callback(triggers, url_params, headers, body)
20 | return [nil, build_response(:ok, nil, url_params[:challenge])] if url_params[:challenge]
21 | [find_matches(triggers, body), build_response(:ok, nil, nil)]
22 | end
23 |
24 | private
25 |
26 | # This sort of method would be specific to your needs.
27 | # For this example Trigger, the data we need can be
28 | # found in the body.
29 | def self.find_matches(triggers, body)
30 | return triggers unless body
31 | payload = json_to_hash(body)
32 |
33 | triggers
34 | end
35 |
36 | def self.json_to_hash(data)
37 | ActiveSupport::JSON.decode(data)
38 | end
39 |
40 | # Support method to build the response hash
41 | def self.build_response(status, headers=nil, body=nil)
42 | {
43 | head_only: true,
44 | status: status,
45 | type: nil, # Can be :json, :xml, :js
46 | headers: headers,
47 | body: body
48 | }
49 | end
50 |
51 |
52 | end
53 | end
54 | end
55 | end
56 | end
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Whenbot::Application.routes.draw do
2 |
3 | # Routes calls to appropriate Trigger
4 | get '/whenbot/:channel/:trigger/callback', to: 'whenbot#callback', as: 'whenbot_callback'
5 |
6 | resources :tasks, :only => [:index, :new]
7 |
8 | # The priority is based upon order of creation:
9 | # first created -> highest priority.
10 |
11 | # Sample of regular route:
12 | # match 'products/:id' => 'catalog#view'
13 | # Keep in mind you can assign values other than :controller and :action
14 |
15 | # Sample of named route:
16 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
17 | # This route can be invoked with purchase_url(:id => product.id)
18 |
19 | # Sample resource route (maps HTTP verbs to controller actions automatically):
20 | # resources :products
21 |
22 | # Sample resource route with options:
23 | # resources :products do
24 | # member do
25 | # get 'short'
26 | # post 'toggle'
27 | # end
28 | #
29 | # collection do
30 | # get 'sold'
31 | # end
32 | # end
33 |
34 | # Sample resource route with sub-resources:
35 | # resources :products do
36 | # resources :comments, :sales
37 | # resource :seller
38 | # end
39 |
40 | # Sample resource route with more complex sub-resources
41 | # resources :products do
42 | # resources :comments
43 | # resources :sales do
44 | # get 'recent', :on => :collection
45 | # end
46 | # end
47 |
48 | # Sample resource route within a namespace:
49 | # namespace :admin do
50 | # # Directs /admin/products/* to Admin::ProductsController
51 | # # (app/controllers/admin/products_controller.rb)
52 | # resources :products
53 | # end
54 |
55 | # You can have the root of your site routed with "root"
56 | # just remember to delete public/index.html.
57 | # root :to => 'welcome#index'
58 |
59 | # See how all your routes lay out with "rake routes"
60 |
61 | # This is a legacy wild controller route that's not recommended for RESTful applications.
62 | # Note: This route will make all actions in every controller accessible via GET requests.
63 | # WARNING: This route will expose all actions in every controller to CSRF attacks.
64 | # DO NOT USE
65 | # match ':controller(/:action(/:id))(.:format)'
66 | end
67 |
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | Whenbot - a personal open source ifttt.com clone. (check out
2 | http://ifttt.com)
3 |
4 | Whenbot is no longer supported!
5 |
6 | Wiki page with more info: https://github.com/ottawaruby/whenbot/wiki/_pages
7 |
8 | simple luuunchbot app: https://github.com/redronin/luuunchbot
9 |
10 |
11 | The idea: A simple web service that runs on Heroku that lets you
12 | listen on certain triggers and perform actions to other web services
13 | based on that.
14 |
15 | For example: Whenever I post an Instagram photo, create a Tumblr blog post.
16 |
17 | It would be personal - in that it's meant for each person who wants to
18 | run it to clone the repo, and host it on their own Heroku instance.
19 | Heroku provides enough free addons and services that you can get it up
20 | and running and functional without paying for anything (or at the
21 | most, a few bucks a month).
22 |
23 | Basic premise is that you have:
24 |
25 | Triggers - things that you listen on (new instagram post, a tweet, a
26 | new email, a new RSS feed entry, etc.). This can be a scheduled
27 | polling to a certain API endpoint (heroku offers regular schedule
28 | tasks every 10 minutes), or webhook endpoints.
29 |
30 | Parsers - things that take the output from a trigger, and
31 | massages/normalizes the data into a known format that can be consumed
32 | by...
33 |
34 | Publishers - things that you publish to (send a tweet, create a Tumblr
35 | post, send an email, etc.)
36 |
37 | The basic engine could be a very simple app, and the Triggers,
38 | Parsers, and Publishers can be individual modules/classes. This would
39 | allow people to work on various triggers, publishers, etc. on their
40 | own while still contributing to the project as a whole. There'd also
41 | be UI work to create the actions, configure services, etc.
42 |
43 | Most triggers and publishers would just be using various API wrappers
44 | for web services like Flickr, Tumblr, Posterous, etc. This would be a
45 | great way for people to get exposed to APIs from various web services.
46 | Some are complicated, some are pretty simple, so there'd be lots to
47 | choose from.
48 |
49 | I created a simple Instagram => Tumblr / Posterous app for
50 | http://luuunch.com which I could show. It uses a ridiculously simple
51 | Tumblr publisher, Posterous publisher and an Instagram webhook
52 | trigger.
53 |
54 | An immediate use case for something like this would be to create an
55 | instagram / flickr / etc. feed to add to the OttawaRuby website.
56 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Whenbot::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # Code is not reloaded between requests
5 | config.cache_classes = true
6 |
7 | # Full error reports are disabled and caching is turned on
8 | config.consider_all_requests_local = false
9 | config.action_controller.perform_caching = true
10 |
11 | # Disable Rails's static asset server (Apache or nginx will already do this)
12 | config.serve_static_assets = false
13 |
14 | # Compress JavaScripts and CSS
15 | config.assets.compress = true
16 |
17 | # Don't fallback to assets pipeline if a precompiled asset is missed
18 | config.assets.compile = false
19 |
20 | # Generate digests for assets URLs
21 | config.assets.digest = true
22 |
23 | # Defaults to Rails.root.join("public/assets")
24 | # config.assets.manifest = YOUR_PATH
25 |
26 | # Specifies the header that your server uses for sending files
27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
29 |
30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
31 | # config.force_ssl = true
32 |
33 | # See everything in the log (default is :info)
34 | # config.log_level = :debug
35 |
36 | # Prepend all log lines with the following tags
37 | # config.log_tags = [ :subdomain, :uuid ]
38 |
39 | # Use a different logger for distributed setups
40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
41 |
42 | # Use a different cache store in production
43 | # config.cache_store = :mem_cache_store
44 |
45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server
46 | # config.action_controller.asset_host = "http://assets.example.com"
47 |
48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
49 | # config.assets.precompile += %w( search.js )
50 |
51 | # Disable delivery errors, bad email addresses will be ignored
52 | # config.action_mailer.raise_delivery_errors = false
53 |
54 | # Enable threaded mode
55 | # config.threadsafe!
56 |
57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
58 | # the I18n.default_locale when a translation can not be found)
59 | config.i18n.fallbacks = true
60 |
61 | # Send deprecation notices to registered listeners
62 | config.active_support.deprecation = :notify
63 |
64 | # Log the query plan for queries taking more than this (works
65 | # with SQLite, MySQL, and PostgreSQL)
66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5
67 | end
68 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | if defined?(Bundler)
6 | # If you precompile assets before deploying to production, use this line
7 | Bundler.require(*Rails.groups(:assets => %w(development test)))
8 | # If you want your assets lazily compiled in production, use this line
9 | # Bundler.require(:default, :assets, Rails.env)
10 | end
11 |
12 | module Whenbot
13 | class Application < Rails::Application
14 | # Settings in config/environments/* take precedence over those specified here.
15 | # Application configuration should go into files in config/initializers
16 | # -- all .rb files in that directory are automatically loaded.
17 |
18 | # Add the X-who-loves-you header to all requests
19 | config.middleware.use 'AddLove'
20 |
21 | # Custom directories with classes and modules you want to be autoloadable.
22 | config.autoload_paths += Dir["#{config.root}/lib"]
23 | # Was: config.autoload_paths += Dir["#{config.root}/lib", "#{config.root}/lib/**/"]
24 |
25 | # Only load the plugins named here, in the order given (default is alphabetical).
26 | # :all can be used as a placeholder for all plugins not explicitly named.
27 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
28 |
29 | # Activate observers that should always be running.
30 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
31 |
32 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
33 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
34 | # config.time_zone = 'Central Time (US & Canada)'
35 |
36 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
37 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
38 | # config.i18n.default_locale = :de
39 |
40 | # Configure the default encoding used in templates for Ruby 1.9.
41 | config.encoding = "utf-8"
42 |
43 | # Configure sensitive parameters which will be filtered from the log file.
44 | config.filter_parameters += [:password]
45 |
46 | # Use SQL instead of Active Record's schema dumper when creating the database.
47 | # This is necessary if your schema can't be completely dumped by the schema dumper,
48 | # like if you have constraints or database-specific column types
49 | # config.active_record.schema_format = :sql
50 |
51 | # Enforce whitelist mode for mass assignment.
52 | # This will create an empty whitelist of attributes available for mass-assignment for all models
53 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
54 | # parameters by using an attr_accessible or attr_protected declaration.
55 | config.active_record.whitelist_attributes = true
56 |
57 | # Enable the asset pipeline
58 | config.assets.enabled = true
59 |
60 | # Version of your assets, change this if you want to expire all your assets
61 | config.assets.version = '1.0'
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/whenbot.rb:
--------------------------------------------------------------------------------
1 | require "whenbot/version"
2 | require "whenbot/channel"
3 | require "whenbot/trigger"
4 |
5 | module Whenbot
6 |
7 | # Note that we can't use autoload because we automatically detect
8 | # Triggers/Actions when a Channel is added via #config. If the
9 | # Triggers aren't loaded, # we can't use #constants to detect the
10 | # Triggers.
11 | #
12 | # autoload :Channel, 'whenbot/channel'
13 | # autoload :Trigger, 'whenbot/trigger'
14 |
15 | # ==== One-liner 1 ====
16 | @@channels = []
17 |
18 | #
19 | # Relays the callbacks that come into the Whenbot#callback
20 | # action to the proper Trigger class.
21 | #
22 | def self.relay_callback(channel, trigger, triggers, url_params, headers, body)
23 | klass = build_class_constant(channel, trigger)
24 | if klass
25 | klass.callback(triggers, url_params, headers, body)
26 | else
27 | :error
28 | end
29 | end
30 |
31 | #
32 | # Handles "config do" block. To be used from intializer.
33 | #
34 | def self.config
35 | channels = yield self
36 | end
37 |
38 | #
39 | # Overloaded accessor to ensure that we always return an array
40 | #
41 | def self.channels
42 | Array(@@channels)
43 | end
44 |
45 | #
46 | # Returns an array of Channels that have at least one Trigger
47 | # Array values are strings. E.g. 'Developer'
48 | #
49 | def self.trigger_channels
50 | trigger_channels_as_consts.collect { |channel| # ==== One-liner 6 ====
51 | }
52 | end
53 |
54 | #
55 | # Returns an array of Trigger Channels as constants. That is,
56 | # any Channels that have at least one Trigger.
57 | #
58 | # E.g. Whenbot::Channels::Developer
59 | #
60 | def self.trigger_channels_as_consts
61 | channels.select do |channel|
62 | has_triggers?(channel)
63 | end
64 | end
65 |
66 | #
67 | # Returns an array of Channels that have at least one Trigger
68 | #
69 | def self.action_channels
70 | # Task: Fill this in
71 | end
72 |
73 | private
74 |
75 |
76 | #
77 | # Helper method to find the constant for the given Channel and Trigger
78 | #
79 | def self.build_class_constant(channel, trigger)
80 | raise ArgumentError, "Need both a Channel and Trigger to match" unless channel && trigger
81 | klass = Whenbot::Channels::const_get(channel.camelize)::Triggers::const_get(trigger.camelize)
82 | rescue NameError => e
83 | # Happens if we can't find the Channel or Trigger.
84 | # Log, email exception (Exceptional? Hoptoad? New Relic?)
85 | Rails.logger.error "[ERROR] NameError. Message = #{e.message}"
86 | raise e
87 | end
88 |
89 |
90 | #
91 | # Checks if there's at least one Trigger defined under the
92 | # Channel's "Triggers" module.
93 | #
94 | def self.has_triggers?(channel)
95 | channel::Triggers::constants.present? if channel.const_defined?(:Triggers)
96 | end
97 |
98 | #
99 | # Helper method to return an array of Trigger's defined for the
100 | # given Channel
101 | #
102 | def self.detect_triggers_for(channel)
103 | channel::Triggers::constants
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/test/unit/triggers/trigger_conformance_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class TriggerConformanceTest < ActiveSupport::TestCase
4 |
5 | setup do
6 | # Pick a trigger to test
7 | @test_trigger = Whenbot::Channels::Developer::Triggers::SampleSearch
8 | end
9 |
10 | #
11 | # Trigger API implementation
12 | #
13 | context "Trigger API implementation" do
14 |
15 | should "include Whenbot::Trigger module" do
16 | assert @test_trigger.included_modules.include?(Whenbot::Trigger), "Doesn't include Whenbot::Trigger module"
17 | end
18 |
19 | # Depends on #set_option being implemented.
20 | # See lib/whenbot/trigger.rb for the task.
21 | #
22 | # should "set use #set_option to set display_title value" do
23 | # assert_not_nil @test_trigger.options[:display_title], "Did not set display title"
24 | # end
25 |
26 | end
27 |
28 | #
29 | # #callback method
30 | #
31 | context "#callback method" do
32 |
33 | should "exist" do
34 | assert_respond_to @test_trigger, :callback, "Doesn't have a #callback method"
35 | end
36 |
37 | end
38 |
39 | #
40 | # #poll? method
41 | #
42 | context "polling" do
43 |
44 | should "have an #is_polling_trigger" do
45 | assert_respond_to @test_trigger, :is_polling_trigger?, "Doesn't have an #is_polling_trigger method"
46 | end
47 |
48 | should "return a true of false value from #is_polling_trigger?" do
49 | result = @test_trigger.is_polling_trigger?
50 | assert result == true || result == false, "Didn't return a true or false value"
51 | end
52 |
53 | should "respond to #poll if #is_polling_trigger? returns true" do
54 | polling = @test_trigger.is_polling_trigger?
55 | if polling
56 | assert_respond_to @test_trigger, :poll, "Doesn't have a #poll method"
57 | end
58 | end
59 |
60 | # Add other tests here, then implement.
61 | #
62 | # See: https://github.com/ottawaruby/whenbot/wiki/Whenbot-Channel-Creation-(Examples)
63 | # for more details.
64 | #
65 | end
66 |
67 | #
68 | # #is_polling_trigger? method
69 | #
70 | context "#is_polling_trigger? method" do
71 |
72 | should "exist" do
73 | assert_respond_to @test_trigger, :is_polling_trigger?, "Doesn't have an #is_polling_trigger? method"
74 | end
75 |
76 | should "return a true of false value" do
77 | result = @test_trigger.is_polling_trigger?
78 | assert result == true || result == false, "Didn't return a true or false value"
79 | end
80 |
81 | # Add other tests here, then implement.
82 | end
83 |
84 | #
85 | # #parameters method
86 | #
87 | context "#parameters method" do
88 |
89 | should "exist" do
90 | assert_respond_to @test_trigger, :parameters, "Doesn't have a #parameters method"
91 | end
92 |
93 | should "return a hash" do
94 | # Task: fill this in
95 | end
96 |
97 | should "contain a :parameters key in the returned hash" do
98 | # Task: fill this in
99 | end
100 |
101 | # Add other tests here, then implement.
102 | end
103 |
104 |
105 |
106 |
107 | end
--------------------------------------------------------------------------------
/app/models/trigger.rb:
--------------------------------------------------------------------------------
1 | class Trigger < ActiveRecord::Base
2 | # ==== One-liner 23 ====
3 |
4 | # ==== One-liner 24 ====
5 | # ==== One-liner 25 ====
6 | # ==== One-liner 26 ====
7 | # ==== One-liner 27 ====
8 | # ==== One-liner 28 ====
9 | # ==== One-liner 29 ====
10 |
11 |
12 | #
13 | # Returns all active triggers for the requested Channel and Trigger
14 | #
15 | def self.triggers_for(channel, trigger)
16 | records = where("channel = ? AND trigger = ?", channel.to_s.camelize, trigger.to_s.camelize).
17 | select([:id, :channel, :trigger, :parameters, :match_data, :extra_data, :last_matched]).
18 | active
19 | convert_for_matching(records)
20 | end
21 |
22 | #
23 | # Saves triggers that either have their :save or
24 | # :matched flag set
25 | def self.save_updated_triggers(triggers, trigger_ids)
26 | save_updated_hash(triggers, trigger_ids)
27 | end
28 |
29 | private
30 |
31 | #
32 | # Creates an array of hashes to be passed to the Trigger
33 | # via its #callback method. The Trigger will fill in
34 | # the array[n][:match_data] if there's a match, and
35 | # can use array[n][:extra_data] to store data that
36 | # it may need later.
37 | #
38 | # Another array of ids is returned to keep track of
39 | # which records should be updated.
40 | #
41 | # We do this to avoid passing around actual records.
42 | # A Trigger only needs to concern itself with certain
43 | # things, and shouldn't be playing with table data.
44 | #
45 | # Hash Values:
46 | #
47 | # :save => Signals that the hash was updated and
48 | # should be saved
49 | # :matched => New match was found, and stored in
50 | # :match_data. Data will be saved
51 | # even if :save isn't set to true
52 | # :parameters => Parameters saved from the user
53 | # :match_data => Match data that's saved to be
54 | # used by the Action (i.e. the
55 | # data to be used by Liquid)
56 | # :extra_data => Use this to store any data
57 | # you want to receive next time
58 | # :last_matched => Time that this Trigger last
59 | # had a match.
60 | def self.convert_for_matching(records)
61 | array = Array.new
62 | ids_array = Array.new
63 |
64 | triggers = Array(records)
65 | triggers.each do |trigger|
66 | hash = Hash.new
67 | hash[:save] = false
68 | hash[:match_found] = false
69 | hash[:parameters] = trigger.parameters || {}
70 | hash[:match_data] = trigger.match_data || {}
71 | hash[:extra_data] = trigger.extra_data || {}
72 | hash[:last_matched] = trigger.last_matched
73 | array << hash
74 | ids_array << trigger.id
75 | end
76 | return array, ids_array
77 | end
78 |
79 | def self.save_updated_hash(triggers, trigger_ids)
80 | triggers.each_with_index do |trigger, index|
81 | if trigger[:save] || trigger[:match]
82 | record = Trigger.find(trigger_ids[index])
83 |
84 | if trigger[:save]
85 | trigger.each_pair do |key, value|
86 | record.send("#{key}=", value) if record.respond_to? "#{key}=".to_sym
87 | end
88 | elsif trigger[:match_found]
89 | record.match_data = trigger[:match_data]
90 | end
91 |
92 | record.save! if record.changed?
93 | end
94 | end
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended to check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(:version => 20120419234122) do
15 |
16 | create_table "actions", :force => true do |t|
17 | t.integer "task_id", :null => false
18 | t.string "channel", :null => false
19 | t.string "action", :null => false
20 | t.text "parameters"
21 | t.text "extra_data"
22 | t.boolean "active", :default => true, :null => false
23 | t.datetime "created_at", :null => false
24 | t.datetime "updated_at", :null => false
25 | end
26 |
27 | add_index "actions", ["active"], :name => "index_actions_on_active"
28 | add_index "actions", ["channel", "action"], :name => "index_actions_on_channel_and_action"
29 | add_index "actions", ["task_id"], :name => "index_actions_on_task_id"
30 |
31 | create_table "authentications", :force => true do |t|
32 | t.string "channel", :null => false
33 | t.text "parameters"
34 | t.datetime "created_at", :null => false
35 | t.datetime "updated_at", :null => false
36 | end
37 |
38 | add_index "authentications", ["channel"], :name => "index_authentications_on_channel", :unique => true
39 |
40 | create_table "tasks", :force => true do |t|
41 | t.string "name"
42 | t.datetime "last_executed"
43 | t.boolean "active", :default => true, :null => false
44 | t.datetime "created_at", :null => false
45 | t.datetime "updated_at", :null => false
46 | end
47 |
48 | add_index "tasks", ["active"], :name => "index_tasks_on_active"
49 |
50 | create_table "triggers", :force => true do |t|
51 | t.integer "task_id", :null => false
52 | t.string "channel", :null => false
53 | t.string "trigger", :null => false
54 | t.boolean "polling_trigger", :default => false, :null => false
55 | t.text "parameters"
56 | t.text "match_data"
57 | t.datetime "last_matched"
58 | t.text "extra_data"
59 | t.string "webhook_uid"
60 | t.boolean "active", :default => true, :null => false
61 | t.datetime "created_at", :null => false
62 | t.datetime "updated_at", :null => false
63 | end
64 |
65 | add_index "triggers", ["active", "polling_trigger"], :name => "index_triggers_on_active_and_polling_trigger"
66 | add_index "triggers", ["active"], :name => "index_triggers_on_active"
67 | add_index "triggers", ["channel", "trigger"], :name => "index_triggers_on_channel_and_trigger"
68 | add_index "triggers", ["polling_trigger"], :name => "index_triggers_on_polling_trigger"
69 | add_index "triggers", ["task_id"], :name => "index_triggers_on_task_id"
70 |
71 | end
72 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: git://github.com/jnicklas/capybara.git
3 | revision: 440d0e1250243e55d12812fa1c03fdb440d7b31d
4 | specs:
5 | capybara (1.1.2)
6 | mime-types (>= 1.16)
7 | nokogiri (>= 1.3.3)
8 | rack (>= 1.0.0)
9 | rack-test (>= 0.5.4)
10 | selenium-webdriver (~> 2.0)
11 | xpath (~> 0.1.4)
12 |
13 | GEM
14 | remote: http://rubygems.org/
15 | specs:
16 | actionmailer (3.2.3)
17 | actionpack (= 3.2.3)
18 | mail (~> 2.4.4)
19 | actionpack (3.2.3)
20 | activemodel (= 3.2.3)
21 | activesupport (= 3.2.3)
22 | builder (~> 3.0.0)
23 | erubis (~> 2.7.0)
24 | journey (~> 1.0.1)
25 | rack (~> 1.4.0)
26 | rack-cache (~> 1.2)
27 | rack-test (~> 0.6.1)
28 | sprockets (~> 2.1.2)
29 | activemodel (3.2.3)
30 | activesupport (= 3.2.3)
31 | builder (~> 3.0.0)
32 | activerecord (3.2.3)
33 | activemodel (= 3.2.3)
34 | activesupport (= 3.2.3)
35 | arel (~> 3.0.2)
36 | tzinfo (~> 0.3.29)
37 | activeresource (3.2.3)
38 | activemodel (= 3.2.3)
39 | activesupport (= 3.2.3)
40 | activesupport (3.2.3)
41 | i18n (~> 0.6)
42 | multi_json (~> 1.0)
43 | addressable (2.2.7)
44 | arel (3.0.2)
45 | awesome_print (1.0.2)
46 | builder (3.0.0)
47 | childprocess (0.3.2)
48 | ffi (~> 1.0.6)
49 | coffee-rails (3.2.2)
50 | coffee-script (>= 2.2.0)
51 | railties (~> 3.2.0)
52 | coffee-script (2.2.0)
53 | coffee-script-source
54 | execjs
55 | coffee-script-source (1.3.1)
56 | commonjs (0.2.6)
57 | database_cleaner (0.7.2)
58 | erubis (2.7.0)
59 | execjs (1.3.0)
60 | multi_json (~> 1.0)
61 | factory_girl (3.2.0)
62 | activesupport (>= 3.0.0)
63 | factory_girl_rails (3.2.0)
64 | factory_girl (~> 3.2.0)
65 | railties (>= 3.0.0)
66 | ffi (1.0.11)
67 | heroku (2.25.0)
68 | launchy (>= 0.3.2)
69 | netrc (~> 0.7.1)
70 | rest-client (~> 1.6.1)
71 | rubyzip
72 | hike (1.2.1)
73 | i18n (0.6.0)
74 | journey (1.0.3)
75 | jquery-rails (2.0.2)
76 | railties (>= 3.2.0, < 5.0)
77 | thor (~> 0.14)
78 | json (1.6.6)
79 | launchy (2.1.0)
80 | addressable (~> 2.2.6)
81 | less (2.2.1)
82 | commonjs (~> 0.2.6)
83 | less-rails (2.2.2)
84 | actionpack (>= 3.1)
85 | less (~> 2.2.0)
86 | less-rails-bootstrap (2.0.12)
87 | less-rails (~> 2.2.0)
88 | libv8 (3.3.10.4)
89 | libwebsocket (0.1.3)
90 | addressable
91 | mail (2.4.4)
92 | i18n (>= 0.4.0)
93 | mime-types (~> 1.16)
94 | treetop (~> 1.4.8)
95 | metaclass (0.0.1)
96 | mime-types (1.18)
97 | mocha (0.11.3)
98 | metaclass (~> 0.0.1)
99 | multi_json (1.3.2)
100 | netrc (0.7.1)
101 | nokogiri (1.5.2)
102 | pg (0.13.2)
103 | polyglot (0.3.3)
104 | rack (1.4.1)
105 | rack-cache (1.2)
106 | rack (>= 0.4)
107 | rack-ssl (1.3.2)
108 | rack
109 | rack-test (0.6.1)
110 | rack (>= 1.0)
111 | rails (3.2.3)
112 | actionmailer (= 3.2.3)
113 | actionpack (= 3.2.3)
114 | activerecord (= 3.2.3)
115 | activeresource (= 3.2.3)
116 | activesupport (= 3.2.3)
117 | bundler (~> 1.0)
118 | railties (= 3.2.3)
119 | rails-footnotes (3.7.8)
120 | rails (>= 3.0.0)
121 | railties (3.2.3)
122 | actionpack (= 3.2.3)
123 | activesupport (= 3.2.3)
124 | rack-ssl (~> 1.3.2)
125 | rake (>= 0.8.7)
126 | rdoc (~> 3.4)
127 | thor (~> 0.14.6)
128 | rake (0.9.2.2)
129 | rdoc (3.12)
130 | json (~> 1.4)
131 | rest-client (1.6.7)
132 | mime-types (>= 1.16)
133 | rubyzip (0.9.7)
134 | sass (3.1.16)
135 | sass-rails (3.2.5)
136 | railties (~> 3.2.0)
137 | sass (>= 3.1.10)
138 | tilt (~> 1.3)
139 | selenium-webdriver (2.21.2)
140 | childprocess (>= 0.2.5)
141 | ffi (~> 1.0)
142 | libwebsocket (~> 0.1.3)
143 | multi_json (~> 1.0)
144 | rubyzip
145 | shoulda (3.0.1)
146 | shoulda-context (~> 1.0.0)
147 | shoulda-matchers (~> 1.0.0)
148 | shoulda-context (1.0.0)
149 | shoulda-matchers (1.0.0)
150 | simple_form (2.0.1)
151 | actionpack (~> 3.0)
152 | activemodel (~> 3.0)
153 | sprockets (2.1.2)
154 | hike (~> 1.2)
155 | rack (~> 1.0)
156 | tilt (~> 1.1, != 1.3.0)
157 | test-unit (2.4.8)
158 | therubyracer (0.10.1)
159 | libv8 (~> 3.3.10)
160 | thor (0.14.6)
161 | tilt (1.3.3)
162 | treetop (1.4.10)
163 | polyglot
164 | polyglot (>= 0.3.1)
165 | tzinfo (0.3.33)
166 | uglifier (1.2.4)
167 | execjs (>= 0.3.0)
168 | multi_json (>= 1.0.2)
169 | xpath (0.1.4)
170 | nokogiri (~> 1.3)
171 |
172 | PLATFORMS
173 | ruby
174 |
175 | DEPENDENCIES
176 | awesome_print
177 | capybara!
178 | coffee-rails (~> 3.2.1)
179 | database_cleaner
180 | factory_girl_rails
181 | heroku
182 | jquery-rails
183 | less-rails-bootstrap
184 | mocha
185 | pg
186 | rails (= 3.2.3)
187 | rails-footnotes (>= 3.7)
188 | sass-rails (~> 3.2.3)
189 | shoulda
190 | simple_form
191 | test-unit
192 | therubyracer
193 | uglifier (>= 1.0.3)
194 |
--------------------------------------------------------------------------------
/test/unit/trigger_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class TriggerTest < ActiveSupport::TestCase
4 |
5 | #
6 | # Options and Configuration
7 | #
8 | context "options and configuration" do
9 |
10 | setup do
11 | @sample_search_trigger = Whenbot::Channels::Developer::Triggers::SampleSearch
12 | end
13 |
14 | should "allow a new option to be set for itself with #option" do
15 | display_title_text = "Sample Search Trigger"
16 | @sample_search_trigger.set_option :display_title, display_title_text
17 | assert_equal display_title_text, @sample_search_trigger.options[:display_title]
18 | end
19 |
20 | should "keep options separate for separate Triggers" do
21 | twitter_trigger = Whenbot::Channels::Twitter::Triggers::NewTweetFrom
22 |
23 | twitter_display_text = "New tweet from"
24 | sample_search_display_text = "Sample Search"
25 |
26 | @sample_search_trigger.set_option :display_title, sample_search_display_text
27 | twitter_trigger.set_option :display_title, twitter_display_text
28 |
29 | assert_not_equal @sample_search_trigger.options[:display_title], twitter_trigger.options[:display_title], "The display titles should be different. Are they sharing the same instance?"
30 | end
31 |
32 | end
33 |
34 | #
35 | # Saved Trigger (model) data
36 | #
37 | # parameters is an array of hashes with the saved parameters, plus
38 | # hashes for :match_data, :extra_data and :last_matched.
39 | #
40 |
41 | context "triggers_for method" do
42 |
43 | setup do
44 | @default_trigger = FactoryGirl.create(:trigger)
45 | @matching_trigger = FactoryGirl.create(:matching_trigger)
46 | @saved_triggers_count = 2
47 | @triggers, @trigger_ids = Trigger.triggers_for('Developer', 'SampleSearch')
48 | end
49 |
50 | should "return an array of Triggers for a given channel and trigger" do
51 | assert_kind_of Array, @triggers, "return value is not an Array"
52 | end
53 |
54 | should "only have one element in the array" do
55 | assert_equal @saved_triggers_count, @triggers.count, "Has more than one element in the array"
56 | end
57 |
58 | should "return an array of Trigger IDs for a given channel and trigger" do
59 | assert_kind_of Array, @trigger_ids, "return value is not an Array"
60 | end
61 |
62 | should "have one ID entry in the array for each Trigger returned" do
63 | assert_equal @triggers.count, @trigger_ids.count, "IDs array didn't contain the same number of elements as the Triggers array"
64 | end
65 |
66 | should "contain a hash" do
67 | @triggers.each do |trigger|
68 | assert_kind_of Hash, trigger, "Array did not contain hashes"
69 | end
70 | end
71 |
72 | should "have a :parameters key for each trigger" do
73 | @triggers.each do |trigger|
74 | assert trigger.has_key?(:parameters), "should have :parameters key"
75 | end
76 | end
77 |
78 | should "populate :parameters hash with saved parameters as a hash" do
79 | @triggers.each do |trigger|
80 | assert_not_nil trigger[:parameters], ":parameters should not be nil"
81 | assert_kind_of Hash, trigger[:parameters], ":parameters key is not a Hash"
82 | end
83 |
84 | @default_trigger.parameters.keys.each do |key|
85 | assert @triggers[0][:parameters].keys.include?(key), ":parameters hash is missing a \"#{key}\" key"
86 | end
87 | end
88 |
89 | should "have an empty hash called :match_data for each trigger" do
90 | @triggers.each do |trigger|
91 | assert trigger.has_key?(:match_data), "trigger didn't have a :match_data key"
92 | assert_kind_of Hash, trigger[:match_data], "trigger[:match_data] wasn't a Hash"
93 | assert trigger[:match_data].empty?, "trigger[:match_data] wasn't empty"
94 | assert_not_nil trigger[:match_data], "trigger[:match_data] should not be nil"
95 | end
96 | end
97 |
98 | should "have a hash called :extra_data for each trigger" do
99 | @triggers.each do |trigger|
100 | # Note: This hash can contain data, if the Trigger saved something there before.
101 | assert trigger.has_key?(:extra_data), "trigger didn't have an :extra_data key"
102 | assert_kind_of Hash, trigger[:extra_data], "trigger[:extra_data] wasn't a Hash"
103 | end
104 | end
105 |
106 | should "have a hash for :last_matched for each trigger" do
107 | @triggers.each do |trigger|
108 | assert trigger.has_key?(:last_matched), "trigger didn't have a :last_matched key"
109 | assert_kind_of ActiveSupport::TimeWithZone, trigger[:last_matched], "trigger[:last_matched] wasn't a Time object"
110 | assert_not_nil trigger[:last_matched], "trigger[:last_matched] was nil"
111 | end
112 | end
113 |
114 | should "return appropriate results with symbols as input" do
115 | assert_nothing_raised do
116 | triggers, trigger_ids = Trigger.triggers_for(:developer, :sample_search)
117 | assert_not_nil triggers, "no triggers were returned"
118 | end
119 | end
120 |
121 | should "return appropriate results with underscorized strings as input" do
122 | assert_nothing_raised do
123 | triggers, trigger_ids = Trigger.triggers_for('developer', 'sample_search')
124 | assert_not_nil triggers, "no triggers were returned"
125 | end
126 | end
127 | end
128 | end
129 |
--------------------------------------------------------------------------------
/test/unit/triggers/sample_search_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class SampleSearchTest < ActiveSupport::TestCase
4 |
5 | def setup
6 | @sample_search_trigger = Whenbot::Channels::Developer::Triggers::SampleSearch
7 |
8 | @default_trigger = FactoryGirl.create(:trigger)
9 | @matching_trigger = FactoryGirl.create(:matching_trigger)
10 |
11 | @search_term = @matching_trigger.parameters[:search_term]
12 | @body_as_hash = { content: "Some text that mentions #{@search_term}" }
13 | @body = @body_as_hash.to_json
14 | @url_params = { url_data: "some data" }
15 | @headers = { 'Content-Type' => 'application/json' }
16 |
17 | @triggers, @trigger_ids = Trigger.triggers_for('Developer', 'SampleSearch')
18 | @returned_triggers, @returned_response = @sample_search_trigger.callback(@triggers, @url_params, @headers, @body)
19 | end
20 |
21 | #
22 | # Trigger API implementation
23 | #
24 | context "trigger API implementation" do
25 |
26 | should "have a #callback method" do
27 | assert_respond_to @sample_search_trigger, :callback, "Doesn't have a #callback method"
28 | end
29 |
30 | end
31 |
32 | #
33 | # Callback method
34 | #
35 | context "callback method" do
36 |
37 | should "return an array of Triggers for a given channel and trigger" do
38 | assert_kind_of Array, @returned_triggers, "return value is not an Array"
39 | end
40 |
41 | should "contain a hash" do
42 | @returned_triggers.each do |trigger|
43 | assert_kind_of Hash, trigger, "Array did not contain hashes"
44 | end
45 | end
46 |
47 | should "have a :parameters key for each trigger" do
48 | @returned_triggers.each do |trigger|
49 | assert trigger.has_key?(:parameters), "should have :parameters key"
50 | end
51 | end
52 |
53 | should "populate :parameters hash with saved parameters as a hash" do
54 | @returned_triggers.each do |trigger|
55 | assert_not_nil trigger[:parameters], ":parameters should not be nil"
56 | assert_kind_of Hash, trigger[:parameters], ":parameters key is not a Hash"
57 | end
58 |
59 | @default_trigger.parameters.keys.each do |key|
60 | assert @triggers[0][:parameters].keys.include?(key), ":parameters hash is missing a \"#{key}\" key"
61 | end
62 | end
63 |
64 | should "still have a hash called :match_data for each trigger" do
65 | @returned_triggers.each do |trigger|
66 | assert trigger.has_key?(:match_data), "trigger didn't have a :match_data key"
67 | assert_kind_of Hash, trigger[:match_data], "trigger[:match_data] wasn't a Hash"
68 | assert_not_nil trigger[:match_data], "trigger[:match_data] should not be nil"
69 | end
70 | end
71 |
72 | should "have a hash called :extra_data for each trigger" do
73 | @returned_triggers.each do |trigger|
74 | # Note: This hash can contain data, if the Trigger saved something there before.
75 | assert trigger.has_key?(:extra_data), "trigger didn't have an :extra_data key"
76 | assert_kind_of Hash, trigger[:extra_data], "trigger[:extra_data] wasn't a Hash"
77 | end
78 | end
79 |
80 | should "have a hash for :last_matched for each trigger" do
81 | @returned_triggers.each do |trigger|
82 | assert trigger.has_key?(:last_matched), "trigger didn't have a :last_matched key"
83 | assert_kind_of ActiveSupport::TimeWithZone, trigger[:last_matched], "trigger[:last_matched] wasn't a Time object"
84 | assert_not_nil trigger[:last_matched], "trigger[:last_matched] was nil"
85 | end
86 | end
87 |
88 | should "return original triggers array if no match found" do
89 | # Update the matching trigger, which would return a match
90 | @matching_trigger.parameters = { search_term: 'no match' }
91 | @matching_trigger.save
92 |
93 | triggers, trigger_ids = Trigger.triggers_for('Developer', 'SampleSearch')
94 | triggers, response = @sample_search_trigger.callback(triggers, @url_params, @headers, @body)
95 |
96 | triggers.each do |trigger|
97 | assert trigger[:match_data].empty?, "Trigger's :match_data reported a match when there was none"
98 | end
99 | end
100 |
101 | should "update the :match_data of a trigger when a match is found" do
102 | triggers, response = @sample_search_trigger.callback(@triggers, @url_params, @headers, @body)
103 | triggers.each do |trigger|
104 | if trigger[:parameters][:search_term] == @search_term
105 | assert_not_nil trigger[:match_data], ":match_data was nil, when it should've been a Hash"
106 | assert !trigger[:match_data].empty?, "no :match_data was returned"
107 | assert trigger[:match_data].has_key?(:content), ":match_data did not have :content key"
108 | end
109 | end
110 | end
111 |
112 | should "return match data if a match is found" do
113 | match_hash_found = false
114 | triggers, response = @sample_search_trigger.callback(@triggers, @url_params, @headers, @body)
115 | triggers.each do |trigger|
116 | if trigger[:parameters][:search_term] == @search_term
117 | match_hash_found = true
118 | assert !trigger[:match_data][:content].empty?
119 | assert_equal @body_as_hash[:content], trigger[:match_data][:content], "Did not find correct match"
120 | end
121 | end
122 | assert match_hash_found, "Didn't find a match"
123 | end
124 |
125 | end
126 |
127 | end
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Ruby on Rails: Welcome aboard
5 |
174 |
187 |
188 |
189 |
190 |
203 |
204 |
205 |
209 |
210 |
214 |
215 |
216 |
Getting started
217 |
Here’s how to get rolling:
218 |
219 |
220 | -
221 |
Use rails generate to create your models and controllers
222 | To see all available options, run it without parameters.
223 |
224 |
225 | -
226 |
Set up a default route and remove public/index.html
227 | Routes are set up in config/routes.rb.
228 |
229 |
230 | -
231 |
Create your database
232 | Run rake db:create to create your database. If you're not using SQLite (the default), edit config/database.yml with your username and password.
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
--------------------------------------------------------------------------------
/config/initializers/simple_form.rb:
--------------------------------------------------------------------------------
1 | # Use this setup block to configure all options available in SimpleForm.
2 | SimpleForm.setup do |config|
3 | # Wrappers are used by the form builder to generate a
4 | # complete input. You can remove any component from the
5 | # wrapper, change the order or even add your own to the
6 | # stack. The options given below are used to wrap the
7 | # whole input.
8 | config.wrappers :default, :class => :input,
9 | :hint_class => :field_with_hint, :error_class => :field_with_errors do |b|
10 | ## Extensions enabled by default
11 | # Any of these extensions can be disabled for a
12 | # given input by passing: `f.input EXTENSION_NAME => false`.
13 | # You can make any of these extensions optional by
14 | # renaming `b.use` to `b.optional`.
15 |
16 | # Determines whether to use HTML5 (:email, :url, ...)
17 | # and required attributes
18 | b.use :html5
19 |
20 | # Calculates placeholders automatically from I18n
21 | # You can also pass a string as f.input :placeholder => "Placeholder"
22 | b.use :placeholder
23 |
24 | ## Optional extensions
25 | # They are disabled unless you pass `f.input EXTENSION_NAME => :lookup`
26 | # to the input. If so, they will retrieve the values from the model
27 | # if any exists. If you want to enable the lookup for any of those
28 | # extensions by default, you can change `b.optional` to `b.use`.
29 |
30 | # Calculates maxlength from length validations for string inputs
31 | b.optional :maxlength
32 |
33 | # Calculates pattern from format validations for string inputs
34 | b.optional :pattern
35 |
36 | # Calculates min and max from length validations for numeric inputs
37 | b.optional :min_max
38 |
39 | # Calculates readonly automatically from readonly attributes
40 | b.optional :readonly
41 |
42 | ## Inputs
43 | b.use :label_input
44 | b.use :hint, :wrap_with => { :tag => :span, :class => :hint }
45 | b.use :error, :wrap_with => { :tag => :span, :class => :error }
46 | end
47 |
48 | config.wrappers :bootstrap, :tag => 'div', :class => 'control-group', :error_class => 'error' do |b|
49 | b.use :html5
50 | b.use :placeholder
51 | b.use :label
52 | b.wrapper :tag => 'div', :class => 'controls' do |ba|
53 | ba.use :input
54 | ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' }
55 | ba.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' }
56 | end
57 | end
58 |
59 | config.wrappers :prepend, :tag => 'div', :class => "control-group", :error_class => 'error' do |b|
60 | b.use :html5
61 | b.use :placeholder
62 | b.use :label
63 | b.wrapper :tag => 'div', :class => 'controls' do |input|
64 | input.wrapper :tag => 'div', :class => 'input-prepend' do |prepend|
65 | prepend.use :input
66 | end
67 | input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' }
68 | input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' }
69 | end
70 | end
71 |
72 | config.wrappers :append, :tag => 'div', :class => "control-group", :error_class => 'error' do |b|
73 | b.use :html5
74 | b.use :placeholder
75 | b.use :label
76 | b.wrapper :tag => 'div', :class => 'controls' do |input|
77 | input.wrapper :tag => 'div', :class => 'input-append' do |append|
78 | append.use :input
79 | end
80 | input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' }
81 | input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' }
82 | end
83 | end
84 |
85 | # Wrappers for forms and inputs using the Twitter Bootstrap toolkit.
86 | # Check the Bootstrap docs (http://twitter.github.com/bootstrap)
87 | # to learn about the different styles for forms and inputs,
88 | # buttons and other elements.
89 | config.default_wrapper = :bootstrap
90 |
91 | # Define the way to render check boxes / radio buttons with labels.
92 | # Defaults to :nested for bootstrap config.
93 | # :inline => input + label
94 | # :nested => label > input
95 | config.boolean_style = :nested
96 |
97 | # Default class for buttons
98 | config.button_class = 'btn'
99 |
100 | # Method used to tidy up errors.
101 | # config.error_method = :first
102 |
103 | # Default tag used for error notification helper.
104 | config.error_notification_tag = :div
105 |
106 | # CSS class to add for error notification helper.
107 | config.error_notification_class = 'alert alert-error'
108 |
109 | # ID to add for error notification helper.
110 | # config.error_notification_id = nil
111 |
112 | # Series of attempts to detect a default label method for collection.
113 | # config.collection_label_methods = [ :to_label, :name, :title, :to_s ]
114 |
115 | # Series of attempts to detect a default value method for collection.
116 | # config.collection_value_methods = [ :id, :to_s ]
117 |
118 | # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none.
119 | # config.collection_wrapper_tag = nil
120 |
121 | # You can define the class to use on all collection wrappers. Defaulting to none.
122 | # config.collection_wrapper_class = nil
123 |
124 | # You can wrap each item in a collection of radio/check boxes with a tag,
125 | # defaulting to :span. Please note that when using :boolean_style = :nested,
126 | # SimpleForm will force this option to be a label.
127 | # config.item_wrapper_tag = :span
128 |
129 | # You can define a class to use in all item wrappers. Defaulting to none.
130 | # config.item_wrapper_class = nil
131 |
132 | # How the label text should be generated altogether with the required text.
133 | # config.label_text = lambda { |label, required| "#{required} #{label}" }
134 |
135 | # You can define the class to use on all labels. Default is nil.
136 | config.label_class = 'control-label'
137 |
138 | # You can define the class to use on all forms. Default is simple_form.
139 | # config.form_class = :simple_form
140 |
141 | # You can define which elements should obtain additional classes
142 | # config.generate_additional_classes_for = [:wrapper, :label, :input]
143 |
144 | # Whether attributes are required by default (or not). Default is true.
145 | # config.required_by_default = true
146 |
147 | # Tell browsers whether to use default HTML5 validations (novalidate option).
148 | # Default is enabled.
149 | config.browser_validations = false
150 |
151 | # Collection of methods to detect if a file type was given.
152 | # config.file_methods = [ :mounted_as, :file?, :public_filename ]
153 |
154 | # Custom mappings for input types. This should be a hash containing a regexp
155 | # to match as key, and the input type that will be used when the field name
156 | # matches the regexp as value.
157 | # config.input_mappings = { /count/ => :integer }
158 |
159 | # Default priority for time_zone inputs.
160 | # config.time_zone_priority = nil
161 |
162 | # Default priority for country inputs.
163 | # config.country_priority = nil
164 |
165 | # Default size for text inputs.
166 | # config.default_input_size = 50
167 |
168 | # When false, do not use translations for labels.
169 | # config.translate_labels = true
170 |
171 | # Automatically discover new inputs in Rails' autoload path.
172 | # config.inputs_discovery = true
173 |
174 | # Cache SimpleForm inputs discovery
175 | # config.cache_discovery = !Rails.env.development?
176 | end
177 |
--------------------------------------------------------------------------------
/lib/whenbot/channels/developer/triggers/sample_search.rb:
--------------------------------------------------------------------------------
1 | module Whenbot
2 | module Channels
3 | module Developer
4 | module Triggers
5 | class SampleSearch
6 |
7 | include Whenbot::Trigger
8 |
9 | # Note that the Channel's display title is taken by
10 | # the module name. In this case: Whenbot::Channels::Twitter
11 | # would yield "Developer" as the Channel name.
12 |
13 | # Task: Enable this functionality. See tests in trigger_test.rb
14 | #
15 | # set_option :display_title, 'SampleSearch trigger'
16 | # set_option :description, 'Does nothing. Always reports a match.'
17 |
18 |
19 | #
20 | # Is this a polling trigger?
21 | #
22 | # Note: When creating a webhook or polling a service, it is
23 | # important to set the callback URL to
24 | # /webhooks///callback
25 | # For example, this Trigger would use:
26 | # /webhooks/developer/sample_search/callback.
27 | #
28 | # Without this, your #callback method will never be called.
29 | #
30 | # - Returns false if this Trigger sets up a webhook to be notified
31 | # of potential matches.
32 | #
33 | # - Returns true if #poll should be called periodically to allow
34 | # this Trigger to check for matches.
35 | #
36 | def self.is_polling_trigger?
37 | true
38 | end
39 |
40 | #
41 | # Run poll. Calls out to Webservice (use your Channel's Service
42 | # class to hold common code for this).
43 | #
44 | # If your service doesn't poll, you don't need this method.
45 | #
46 | # triggers [Array] => An array of hashes.
47 | # - Each hash contains the same keys/values as those given
48 | # in the #callback method.
49 | #
50 | def self.poll(triggers)
51 | #
52 | # Poll the service.
53 | # If you get the response back immediately
54 | # [Do pretty much the same thing as #callback]
55 | # - Check results
56 | # - Update the triggers array of hashes with any match_data found
57 | # - Set the appropriate flags to signal if Whenbot should save
58 | # the updated data to the database
59 | #
60 | # If you receive the response back via a callback, be sure
61 | # to set your path to /whenbot///callback
62 | #
63 | # For example, this Trigger would use:
64 | # /whenbot/developer/sample_search/callback
65 | #
66 | end
67 |
68 | # Optional. Only needed if this is Trigger can be watched via a webhook.
69 | #
70 | # Creates a webhook on the server for this Trigger, with
71 | # the given params.
72 | #
73 | # trigger => Trigger hash for this Trigger. Same as
74 | # the hash passed into #callback.
75 | #
76 | # Returns: wehbook_uid (string) => an identifier given by the webservice,
77 | # that is used to uniquely identify this hook.
78 | #
79 | def self.create_webhook_for(trigger)
80 | #
81 | # Note: When creating a webhook or polling a service, it is
82 | # important to set the callback URL to
83 | # /whenbot///callback
84 | # For example, this Trigger would use:
85 | # /whenbot/developer/sample_search/callback
86 | #
87 | end
88 |
89 | #
90 | # Cancels a webhook that has been setup on the server.
91 | # Called when a User deletes or deactivates a Trigger.
92 | #
93 | # webhook_uid => The unique id returned from create_webhook_for
94 | # trigger => Trigger hash for this Trigger. Same as
95 | # the hash passed into #callback.
96 | #
97 | # Returns: true or false.
98 | #
99 | def self.cancel_webhook_for(webhook_uid, trigger)
100 | # Optional. Only needed if this Trigger uses webhooks.
101 | end
102 |
103 | #
104 | # A form will be automatically generated when the User is
105 | # creating a Task, to get the required parameters.
106 | #
107 | # Returns: Hash of parameters to be obtained from the
108 | # user when setting up this Trigger, and then saved
109 | # to the database.
110 | #
111 | # @return [Hash]
112 | # Hash of the following form:
113 | #
114 | # {
115 | # parameter_name: {
116 | # label: ,
117 | # input_type: , # can be :text_field, :select, :checkbox, etc.
118 | # help_text: # optional
119 | # optional: # Is this parameter mandatory?
120 | # template: # Should be parsed for Liquid data
121 | # }
122 | # }
123 | #
124 | #
125 | def self.parameters
126 | {
127 | search_term: {
128 | label: 'Search term',
129 | input_type: :text,
130 | help_text: 'Text to be reported back as the matched data',
131 | optional: false,
132 | accepts_liquid: true
133 | }
134 | }
135 | end
136 |
137 | #
138 | # Called by Whenbot whenever a new response is received
139 | # from a web service for this Trigger.
140 | #
141 | # triggers [Array] => An array of hashes. Each hash contains:
142 | #
143 | # hash[:parameters] # => saved parameters
144 | #
145 | # hash[:match_data] # => an empty hash, for you to fill with
146 | # your match data if a match is found.
147 | #
148 | # hash[:extra_data] # => Use this to store/retrieve custom data
149 | # that you need from callback to callback.
150 | # Starts out empty, saves what you put in
151 | # and returns the previously saved data
152 | # with each call to #callback.
153 | #
154 | # hash[:last_matched] # => The last date and time the Trigger was
155 | # matched. Will be updated by Whenbot if
156 | # you update the :matched_data hash.
157 | #
158 | # parameters [Hash] => params hash
159 | # headers [Hash] => Result of request.headers
160 | # body [String] => Result of request.body.read
161 | #
162 | # returns:
163 | # - The original trigger_params array, but with
164 | # updated :match_data where appropriate
165 | # - An array containing [:status, :headers, :body] for
166 | # the response to the server.
167 | #
168 | def self.callback(triggers, url_params, headers, body)
169 | [find_matches(triggers, body), build_response(:ok)]
170 | end
171 |
172 | private
173 |
174 | # This sort of method would be specific to your needs.
175 | # For this example Trigger, the data we need can be
176 | # found in the body.
177 | def self.find_matches(triggers, body)
178 | return triggers unless body
179 | payload = json_to_hash(body)
180 |
181 | triggers.each do |trigger|
182 | if payload['content'].include? trigger[:parameters][:search_term]
183 | # Update the current trigger, since we have a match
184 | # This is how we signal a match.
185 | #
186 | # Note: When updating :last_matched, use the #current_time
187 | # method, to ensure that all times are in the correct timezone.
188 |
189 | trigger[:match_found] = true
190 | trigger[:match_data] = { content: payload['content'] }
191 | trigger[:last_updated] = current_time # To maintain integrity
192 | trigger[:save] = true
193 | end
194 | end
195 |
196 | triggers
197 | end
198 |
199 | def self.json_to_hash(data)
200 | ActiveSupport::JSON.decode(data)
201 | end
202 |
203 | # Support method to build the response hash
204 | #
205 | # For this example, we either return the challenge (sometimes
206 | # requested by a web service), or just return a status code.
207 | def self.build_response(status, headers=nil, body=nil)
208 | {
209 | head_only: true,
210 | status: status,
211 | type: nil, # Can be :json, :xml, :js
212 | headers: headers,
213 | body: body
214 | }
215 | end
216 | end
217 | end
218 | end
219 | end
220 | end
221 |
--------------------------------------------------------------------------------