├── .gitignore ├── .rvmrc ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README ├── Rakefile ├── app ├── assets │ ├── images │ │ └── rails.png │ ├── javascripts │ │ ├── application.js │ │ ├── tasks.js.coffee │ │ └── whenbot.js.coffee │ └── stylesheets │ │ ├── application.css │ │ ├── bootstrap_config.css.less │ │ ├── tasks.css.scss │ │ └── whenbot.css.scss ├── controllers │ ├── application_controller.rb │ ├── tasks_controller.rb │ └── whenbot_controller.rb ├── helpers │ ├── application_helper.rb │ ├── tasks_helper.rb │ └── whenbot_helper.rb ├── mailers │ └── .gitkeep ├── models │ ├── .gitkeep │ ├── action.rb │ ├── authentication.rb │ ├── task.rb │ └── trigger.rb └── views │ ├── layouts │ ├── _notice.html.erb │ └── application.html.erb │ └── tasks │ ├── _channels.html.erb │ ├── index.html.erb │ └── new.html.erb ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml.example ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── add_love.rb │ ├── backtrace_silencers.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── require_lib_files.rb │ ├── secret_token.rb │ ├── session_store.rb │ ├── simple_form.rb │ ├── whenbot.rb │ └── wrap_parameters.rb ├── locales │ ├── en.yml │ └── simple_form.en.yml └── routes.rb ├── db ├── migrate │ ├── 20120419221210_create_triggers.rb │ ├── 20120419230901_create_actions.rb │ ├── 20120419233318_create_authentications.rb │ └── 20120419234122_create_tasks.rb ├── schema.rb └── seeds.rb ├── doc └── README_FOR_APP ├── lib ├── assets │ └── .gitkeep ├── tasks │ └── .gitkeep ├── templates │ └── erb │ │ └── scaffold │ │ └── _form.html.erb ├── whenbot.rb └── whenbot │ ├── channel.rb │ ├── channels │ ├── developer │ │ ├── actions │ │ │ └── do_some_event.rb │ │ ├── service.rb │ │ └── triggers │ │ │ └── sample_search.rb │ └── twitter │ │ ├── service.rb │ │ └── triggers │ │ └── new_tweet_from.rb │ ├── trigger.rb │ └── version.rb ├── log └── .gitkeep ├── public ├── 404.html ├── 422.html ├── 500.html ├── favicon.ico ├── index.html └── robots.txt ├── script └── rails ├── test ├── factories │ ├── actions.rb │ ├── authentications.rb │ ├── tasks.rb │ └── triggers.rb ├── functional │ ├── .gitkeep │ ├── tasks_controller_test.rb │ └── whenbot_controller_test.rb ├── integration │ ├── .gitkeep │ └── task_flows_test.rb ├── performance │ └── browsing_test.rb ├── test_helper.rb └── unit │ ├── .gitkeep │ ├── action_test.rb │ ├── authentication_test.rb │ ├── helpers │ ├── tasks_helper_test.rb │ └── whenbot_helper_test.rb │ ├── task_test.rb │ ├── trigger_test.rb │ ├── triggers │ ├── sample_search_test.rb │ └── trigger_conformance_test.rb │ └── whenbot_test.rb └── vendor ├── assets ├── javascripts │ └── .gitkeep └── stylesheets │ └── .gitkeep └── plugins └── .gitkeep /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/assets/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottawaruby/whenbot/492340255689c15df2384bf8c284dd2567c91ec1/app/assets/images/rails.png -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/tasks_helper.rb: -------------------------------------------------------------------------------- 1 | module TasksHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/whenbot_helper.rb: -------------------------------------------------------------------------------- 1 | module WhenbotHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/mailers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottawaruby/whenbot/492340255689c15df2384bf8c284dd2567c91ec1/app/mailers/.gitkeep -------------------------------------------------------------------------------- /app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottawaruby/whenbot/492340255689c15df2384bf8c284dd2567c91ec1/app/models/.gitkeep -------------------------------------------------------------------------------- /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/models/authentication.rb: -------------------------------------------------------------------------------- 1 | class Authentication < ActiveRecord::Base 2 | serialize :parameters, Hash 3 | attr_accessible :channel, :parameters 4 | end 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/views/layouts/_notice.html.erb: -------------------------------------------------------------------------------- 1 |
2 | x 3 | <%= flash[:notice] %> 4 |
-------------------------------------------------------------------------------- /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 | 46 | 47 |
48 | <%= render 'layouts/notice' unless notice.blank? %> 49 | <%= yield %> 50 | 51 |
52 |
53 |

54 | ♥ WHENBOT LOVES YOU ♥ 55 |

56 |
57 |
58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /app/views/tasks/_channels.html.erb: -------------------------------------------------------------------------------- 1 | <% channels.each do |channel| %> 2 |
3 |

<%= channel %>

4 |
5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/tasks/index.html.erb: -------------------------------------------------------------------------------- 1 |

Tasks#index

2 |

Find me in app/views/tasks/index.html.erb

3 | -------------------------------------------------------------------------------- /app/views/tasks/new.html.erb: -------------------------------------------------------------------------------- 1 |

Create New Task

2 |
3 |

Trigger Channels

4 | <%= render 'channels', channels: @channels %> 5 |
6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottawaruby/whenbot/492340255689c15df2384bf8c284dd2567c91ec1/lib/assets/.gitkeep -------------------------------------------------------------------------------- /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottawaruby/whenbot/492340255689c15df2384bf8c284dd2567c91ec1/lib/tasks/.gitkeep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/whenbot/channel.rb: -------------------------------------------------------------------------------- 1 | module Whenbot 2 | module Channel 3 | 4 | end 5 | end -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/whenbot/version.rb: -------------------------------------------------------------------------------- 1 | module Whenbot 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottawaruby/whenbot/492340255689c15df2384bf8c284dd2567c91ec1/log/.gitkeep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottawaruby/whenbot/492340255689c15df2384bf8c284dd2567c91ec1/public/favicon.ico -------------------------------------------------------------------------------- /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 |
  1. 221 |

    Use rails generate to create your models and controllers

    222 |

    To see all available options, run it without parameters.

    223 |
  2. 224 | 225 |
  3. 226 |

    Set up a default route and remove public/index.html

    227 |

    Routes are set up in config/routes.rb.

    228 |
  4. 229 | 230 |
  5. 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 |
  6. 234 |
235 |
236 |
237 | 238 | 239 |
240 | 241 | 242 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /test/functional/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottawaruby/whenbot/492340255689c15df2384bf8c284dd2567c91ec1/test/functional/.gitkeep -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /test/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottawaruby/whenbot/492340255689c15df2384bf8c284dd2567c91ec1/test/integration/.gitkeep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottawaruby/whenbot/492340255689c15df2384bf8c284dd2567c91ec1/test/unit/.gitkeep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/unit/helpers/tasks_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TasksHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/unit/helpers/whenbot_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class WhenbotHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /vendor/assets/javascripts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottawaruby/whenbot/492340255689c15df2384bf8c284dd2567c91ec1/vendor/assets/javascripts/.gitkeep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottawaruby/whenbot/492340255689c15df2384bf8c284dd2567c91ec1/vendor/assets/stylesheets/.gitkeep -------------------------------------------------------------------------------- /vendor/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottawaruby/whenbot/492340255689c15df2384bf8c284dd2567c91ec1/vendor/plugins/.gitkeep --------------------------------------------------------------------------------