@@ -1,5 +1,24 @@ |
||
| 1 | 1 |
source 'https://rubygems.org' |
| 2 | 2 |
|
| 3 |
+# Optional libraries. To conserve RAM, comment out any that you don't need, |
|
| 4 |
+# then run `bundle` and commit the updated Gemfile and Gemfile.lock. |
|
| 5 |
+gem 'twilio-ruby', '~> 3.11.5' # TwilioAgent |
|
| 6 |
+gem 'ruby-growl', '~> 4.1.0' # GrowlAgent |
|
| 7 |
+gem 'net-ftp-list', '~> 3.2.8' # FtpsiteAgent |
|
| 8 |
+gem 'wunderground', '~> 1.2.0' # WeatherAgent |
|
| 9 |
+gem 'forecast_io', '~> 2.0.0' # WeatherAgent |
|
| 10 |
+gem 'rturk', '~> 2.12.1' # HumanTaskAgent |
|
| 11 |
+gem 'weibo_2', '~> 0.1.4' # Weibo Agents |
|
| 12 |
+gem 'hipchat', '~> 1.2.0' # HipchatAgent |
|
| 13 |
+gem 'xmpp4r', '~> 0.5.6' # JabberAgent |
|
| 14 |
+gem "google-api-client" # GoogleCalendarPublishAgent |
|
| 15 |
+gem 'mqtt' # MQTTAgent |
|
| 16 |
+gem 'slack-notifier', '~> 0.5.0' # SlackAgent |
|
| 17 |
+ |
|
| 18 |
+# Optional Services. |
|
| 19 |
+gem 'omniauth-37signals' # BasecampAgent |
|
| 20 |
+# gem 'omniauth-github' |
|
| 21 |
+ |
|
| 3 | 22 |
# Bundler <1.5 does not recognize :x64_mingw as a valid platform name. |
| 4 | 23 |
# Unfortunately, it can't self-update because it errors when encountering :x64_mingw. |
| 5 | 24 |
unless Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('1.5.0')
|
@@ -7,109 +26,68 @@ unless Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('1.5.0')
|
||
| 7 | 26 |
exit 1 |
| 8 | 27 |
end |
| 9 | 28 |
|
| 10 |
-gem 'bundler', '>= 1.5.0' |
|
| 11 |
- |
|
| 12 |
-gem 'protected_attributes', '~>1.0.8' |
|
| 13 |
- |
|
| 14 |
-gem 'rails' , '4.1.5' |
|
| 15 |
- |
|
| 16 |
-case RUBY_PLATFORM |
|
| 17 |
-when /freebsd|netbsd|openbsd/ |
|
| 18 |
- # ffi (required by typhoeus via ethon) merged fixes for bugs fatal |
|
| 19 |
- # on these platforms after 1.9.3; no following release as yet. |
|
| 20 |
- gem 'ffi', github: 'ffi/ffi', branch: 'master' |
|
| 21 |
- |
|
| 22 |
- # tzinfo 1.2.0 has added support for reading zoneinfo on these |
|
| 23 |
- # platforms. |
|
| 24 |
- gem 'tzinfo', '>= 1.2.0' |
|
| 25 |
-when /solaris/ |
|
| 26 |
- # ditto |
|
| 27 |
- gem 'tzinfo', '>= 1.2.0' |
|
| 28 |
-end |
|
| 29 |
- |
|
| 30 |
-# Windows does not have zoneinfo files, so bundle the tzinfo-data gem. |
|
| 31 |
-gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw] |
|
| 29 |
+gem 'protected_attributes', '~>1.0.8' # This must be loaded before some other gems, like delayed_job. |
|
| 32 | 30 |
|
| 33 |
-gem 'mysql2', '~> 0.3.16' |
|
| 34 |
-gem 'devise', '~> 3.2.4' |
|
| 35 |
-gem 'kaminari', '~> 0.16.1' |
|
| 31 |
+gem 'ace-rails-ap', '~> 2.0.1' |
|
| 36 | 32 |
gem 'bootstrap-kaminari-views', '~> 0.0.3' |
| 37 |
-gem 'rufus-scheduler', '~> 3.0.8', require: false |
|
| 38 |
-gem 'json', '~> 1.8.1' |
|
| 39 |
-gem 'jsonpath', '~> 0.5.6' |
|
| 40 |
-gem 'twilio-ruby', '~> 3.11.5' |
|
| 41 |
-gem 'ruby-growl', '~> 4.1.0' |
|
| 42 |
-gem 'liquid', '~> 2.6.1' |
|
| 43 |
- |
|
| 33 |
+gem 'bundler', '>= 1.5.0' |
|
| 34 |
+gem 'cantino-twitter-stream', github: 'cantino/twitter-stream', branch: 'master' |
|
| 35 |
+gem 'coffee-rails', '~> 4.0.0' |
|
| 36 |
+gem 'daemons', '~> 1.1.9' |
|
| 44 | 37 |
gem 'delayed_job', '~> 4.0.0' |
| 45 | 38 |
gem 'delayed_job_active_record', '~> 4.0.0' |
| 46 |
-gem 'daemons', '~> 1.1.9' |
|
| 47 |
- |
|
| 39 |
+gem 'devise', '~> 3.2.4' |
|
| 40 |
+gem 'em-http-request', '~> 1.1.2' |
|
| 41 |
+gem 'faraday', '~> 0.9.0' |
|
| 42 |
+gem 'faraday_middleware' |
|
| 43 |
+gem 'feed-normalizer' |
|
| 48 | 44 |
gem 'foreman', '~> 0.63.0' |
| 49 |
- |
|
| 50 |
-gem 'sass-rails', '~> 4.0.0' |
|
| 51 |
-gem 'coffee-rails', '~> 4.0.0' |
|
| 52 |
-gem 'uglifier', '>= 1.3.0' |
|
| 53 |
-gem 'select2-rails', '~> 3.5.4' |
|
| 54 |
-gem 'jquery-rails', '~> 3.1.0' |
|
| 55 |
-gem 'ace-rails-ap', '~> 2.0.1' |
|
| 56 |
-gem 'spectrum-rails' |
|
| 57 |
- |
|
| 58 |
- |
|
| 59 | 45 |
# geokit-rails doesn't work with geokit 1.8.X but it specifies ~> 1.5 |
| 60 | 46 |
# in its own Gemfile. |
| 61 | 47 |
gem 'geokit', '~> 1.8.4' |
| 62 | 48 |
gem 'geokit-rails', '~> 2.0.1' |
| 63 |
- |
|
| 49 |
+gem 'httparty', '~> 0.13' |
|
| 50 |
+gem 'jquery-rails', '~> 3.1.0' |
|
| 51 |
+gem 'json', '~> 1.8.1' |
|
| 52 |
+gem 'jsonpath', '~> 0.5.6' |
|
| 53 |
+gem 'kaminari', '~> 0.16.1' |
|
| 64 | 54 |
gem 'kramdown', '~> 1.3.3' |
| 65 |
-gem 'faraday', '~> 0.9.0' |
|
| 66 |
-gem 'faraday_middleware' |
|
| 67 |
-gem 'typhoeus', '~> 0.6.3' |
|
| 55 |
+gem 'liquid', '~> 2.6.1' |
|
| 56 |
+gem 'mysql2', '~> 0.3.16' |
|
| 57 |
+gem 'multi_xml' |
|
| 68 | 58 |
gem 'nokogiri', '~> 1.6.1' |
| 69 |
-gem 'net-ftp-list', '~> 3.2.8' |
|
| 70 |
- |
|
| 71 |
-gem 'wunderground', '~> 1.2.0' |
|
| 72 |
-gem 'forecast_io', '~> 2.0.0' |
|
| 73 |
-gem 'rturk', '~> 2.12.1' |
|
| 74 |
- |
|
| 75 |
-gem "google-api-client" |
|
| 76 |
- |
|
| 77 |
-gem 'twitter', '~> 5.8.0' |
|
| 78 |
-gem 'cantino-twitter-stream', github: 'cantino/twitter-stream', branch: 'master' |
|
| 79 |
-gem 'em-http-request', '~> 1.1.2' |
|
| 80 |
-gem 'weibo_2', '~> 0.1.4' |
|
| 81 |
-gem 'hipchat', '~> 1.2.0' |
|
| 82 |
-gem 'xmpp4r', '~> 0.5.6' |
|
| 83 |
-gem 'feed-normalizer' |
|
| 84 |
-gem 'slack-notifier', '~> 0.5.0' |
|
| 85 |
-gem 'therubyracer', '~> 0.12.1' |
|
| 86 |
-gem 'mqtt' |
|
| 87 |
- |
|
| 88 | 59 |
gem 'omniauth' |
| 89 | 60 |
gem 'omniauth-twitter' |
| 90 |
-gem 'omniauth-37signals' |
|
| 91 |
-gem 'omniauth-github' |
|
| 61 |
+gem 'rails' , '4.1.5' |
|
| 62 |
+gem 'rufus-scheduler', '~> 3.0.8', require: false |
|
| 63 |
+gem 'sass-rails', '~> 4.0.0' |
|
| 64 |
+gem 'select2-rails', '~> 3.5.4' |
|
| 65 |
+gem 'spectrum-rails' |
|
| 66 |
+gem 'therubyracer', '~> 0.12.1' |
|
| 67 |
+gem 'twitter', '~> 5.8.0' |
|
| 68 |
+gem 'typhoeus', '~> 0.6.3' |
|
| 69 |
+gem 'uglifier', '>= 1.3.0' |
|
| 92 | 70 |
|
| 93 | 71 |
group :development do |
| 94 |
- gem 'binding_of_caller' |
|
| 95 | 72 |
gem 'better_errors', '~> 1.1' |
| 73 |
+ gem 'binding_of_caller' |
|
| 96 | 74 |
gem 'quiet_assets' |
| 97 | 75 |
end |
| 98 | 76 |
|
| 99 | 77 |
group :development, :test do |
| 100 |
- gem 'vcr' |
|
| 78 |
+ gem 'coveralls', require: false |
|
| 79 |
+ gem 'delorean' |
|
| 101 | 80 |
gem 'dotenv-rails' |
| 102 | 81 |
gem 'pry' |
| 103 |
- gem 'rspec-rails', '~> 2.99' |
|
| 82 |
+ gem 'rr' |
|
| 104 | 83 |
gem 'rspec', '~> 2.99' |
| 105 | 84 |
gem 'rspec-collection_matchers' |
| 85 |
+ gem 'rspec-rails', '~> 2.99' |
|
| 106 | 86 |
gem 'shoulda-matchers' |
| 107 |
- gem 'rr' |
|
| 108 |
- gem 'delorean' |
|
| 109 |
- gem 'webmock', '~> 1.17.4', require: false |
|
| 110 |
- gem 'coveralls', require: false |
|
| 111 | 87 |
gem 'spring' |
| 112 | 88 |
gem 'spring-commands-rspec' |
| 89 |
+ gem 'vcr' |
|
| 90 |
+ gem 'webmock', '~> 1.17.4', require: false |
|
| 113 | 91 |
end |
| 114 | 92 |
|
| 115 | 93 |
group :production do |
@@ -117,6 +95,23 @@ group :production do |
||
| 117 | 95 |
gem 'rack' |
| 118 | 96 |
end |
| 119 | 97 |
|
| 98 |
+case RUBY_PLATFORM |
|
| 99 |
+ when /freebsd|netbsd|openbsd/ |
|
| 100 |
+ # ffi (required by typhoeus via ethon) merged fixes for bugs fatal |
|
| 101 |
+ # on these platforms after 1.9.3; no following release as yet. |
|
| 102 |
+ gem 'ffi', github: 'ffi/ffi', branch: 'master' |
|
| 103 |
+ |
|
| 104 |
+ # tzinfo 1.2.0 has added support for reading zoneinfo on these |
|
| 105 |
+ # platforms. |
|
| 106 |
+ gem 'tzinfo', '>= 1.2.0' |
|
| 107 |
+ when /solaris/ |
|
| 108 |
+ # ditto |
|
| 109 |
+ gem 'tzinfo', '>= 1.2.0' |
|
| 110 |
+end |
|
| 111 |
+ |
|
| 112 |
+# Windows does not have zoneinfo files, so bundle the tzinfo-data gem. |
|
| 113 |
+gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw] |
|
| 114 |
+ |
|
| 120 | 115 |
# This hack needs some explanation. When on Heroku, use the pg, unicorn, and rails12factor gems. |
| 121 | 116 |
# When not on Heroku, we still want our Gemfile.lock to include these gems, so we scope them to |
| 122 | 117 |
# an unsupported platform. |
@@ -204,9 +204,6 @@ GEM |
||
| 204 | 204 |
omniauth-37signals (1.0.5) |
| 205 | 205 |
omniauth (~> 1.0) |
| 206 | 206 |
omniauth-oauth2 (~> 1.0) |
| 207 |
- omniauth-github (1.1.2) |
|
| 208 |
- omniauth (~> 1.0) |
|
| 209 |
- omniauth-oauth2 (~> 1.1) |
|
| 210 | 207 |
omniauth-oauth (1.0.1) |
| 211 | 208 |
oauth |
| 212 | 209 |
omniauth (~> 1.0) |
@@ -416,6 +413,7 @@ DEPENDENCIES |
||
| 416 | 413 |
geokit-rails (~> 2.0.1) |
| 417 | 414 |
google-api-client |
| 418 | 415 |
hipchat (~> 1.2.0) |
| 416 |
+ httparty (~> 0.13) |
|
| 419 | 417 |
jquery-rails (~> 3.1.0) |
| 420 | 418 |
json (~> 1.8.1) |
| 421 | 419 |
jsonpath (~> 0.5.6) |
@@ -423,12 +421,12 @@ DEPENDENCIES |
||
| 423 | 421 |
kramdown (~> 1.3.3) |
| 424 | 422 |
liquid (~> 2.6.1) |
| 425 | 423 |
mqtt |
| 424 |
+ multi_xml |
|
| 426 | 425 |
mysql2 (~> 0.3.16) |
| 427 | 426 |
net-ftp-list (~> 3.2.8) |
| 428 | 427 |
nokogiri (~> 1.6.1) |
| 429 | 428 |
omniauth |
| 430 | 429 |
omniauth-37signals |
| 431 |
- omniauth-github |
|
| 432 | 430 |
omniauth-twitter |
| 433 | 431 |
pg |
| 434 | 432 |
protected_attributes (~> 1.0.8) |
@@ -175,7 +175,7 @@ span.not-applicable:after {
|
||
| 175 | 175 |
|
| 176 | 176 |
// Disabled |
| 177 | 177 |
|
| 178 |
-.agent-disabled {
|
|
| 178 |
+.agent-unavailable {
|
|
| 179 | 179 |
opacity: 0.5; |
| 180 | 180 |
} |
| 181 | 181 |
|
@@ -5,7 +5,7 @@ module TwitterConcern |
||
| 5 | 5 |
include Oauthable |
| 6 | 6 |
|
| 7 | 7 |
validate :validate_twitter_options |
| 8 |
- valid_oauth_providers :twitter |
|
| 8 |
+ valid_oauth_providers 'twitter' |
|
| 9 | 9 |
end |
| 10 | 10 |
|
| 11 | 11 |
def validate_twitter_options |
@@ -2,6 +2,8 @@ module WeiboConcern |
||
| 2 | 2 |
extend ActiveSupport::Concern |
| 3 | 3 |
|
| 4 | 4 |
included do |
| 5 |
+ gem_dependency_check { defined?(WeiboOAuth2) }
|
|
| 6 |
+ |
|
| 5 | 7 |
self.validate :validate_weibo_options |
| 6 | 8 |
end |
| 7 | 9 |
|
@@ -22,8 +24,4 @@ module WeiboConcern |
||
| 22 | 24 |
end |
| 23 | 25 |
@weibo_client |
| 24 | 26 |
end |
| 25 |
- |
|
| 26 |
- module ClassMethods |
|
| 27 |
- |
|
| 28 |
- end |
|
| 29 | 27 |
end |
@@ -32,6 +32,8 @@ module ApplicationHelper |
||
| 32 | 32 |
def working(agent) |
| 33 | 33 |
if agent.disabled? |
| 34 | 34 |
link_to 'Disabled', agent_path(agent), class: 'label label-warning' |
| 35 |
+ elsif agent.dependencies_missing? |
|
| 36 |
+ content_tag :span, 'Missing Gems', class: 'label label-danger' |
|
| 35 | 37 |
elsif agent.working? |
| 36 | 38 |
content_tag :span, 'Yes', class: 'label label-success' |
| 37 | 39 |
else |
@@ -137,9 +137,9 @@ module DotHelper |
||
| 137 | 137 |
label: agent_label[agent], |
| 138 | 138 |
tooltip: (agent.short_type.titleize if rich), |
| 139 | 139 |
URL: (agent_url[agent] if rich), |
| 140 |
- style: ('rounded,dashed' if agent.disabled?),
|
|
| 141 |
- color: (@disabled if agent.disabled?), |
|
| 142 |
- fontcolor: (@disabled if agent.disabled?)) |
|
| 140 |
+ style: ('rounded,dashed' if agent.unavailable?),
|
|
| 141 |
+ color: (@disabled if agent.unavailable?), |
|
| 142 |
+ fontcolor: (@disabled if agent.unavailable?)) |
|
| 143 | 143 |
end |
| 144 | 144 |
|
| 145 | 145 |
def agent_edge(agent, receiver) |
@@ -148,7 +148,7 @@ module DotHelper |
||
| 148 | 148 |
style: ('dashed' unless receiver.propagate_immediately?),
|
| 149 | 149 |
label: (" #{agent.control_action}s " if agent.can_control_other_agents?),
|
| 150 | 150 |
arrowhead: ('empty' if agent.can_control_other_agents?),
|
| 151 |
- color: (@disabled if agent.disabled? || receiver.disabled?)) |
|
| 151 |
+ color: (@disabled if agent.unavailable? || receiver.unavailable?)) |
|
| 152 | 152 |
end |
| 153 | 153 |
|
| 154 | 154 |
block('digraph', 'Agent Event Flow') {
|
@@ -218,7 +218,7 @@ module DotHelper |
||
| 218 | 218 |
# a dummy label only to obtain the background color |
| 219 | 219 |
label['class'] = [ |
| 220 | 220 |
'label', |
| 221 |
- if agent.disabled? |
|
| 221 |
+ if agent.unavailable? |
|
| 222 | 222 |
'label-warning' |
| 223 | 223 |
elsif agent.working? |
| 224 | 224 |
'label-success' |
@@ -1,5 +0,0 @@ |
||
| 1 |
-module ServiceHelper |
|
| 2 |
- def has_oauth_configuration_for(provider) |
|
| 3 |
- ENV["#{provider.upcase}_OAUTH_KEY"].present? && ENV["#{provider.upcase}_OAUTH_SECRET"].present?
|
|
| 4 |
- end |
|
| 5 |
-end |
@@ -150,6 +150,14 @@ class Agent < ActiveRecord::Base |
||
| 150 | 150 |
end |
| 151 | 151 |
end |
| 152 | 152 |
|
| 153 |
+ def unavailable? |
|
| 154 |
+ disabled? || dependencies_missing? |
|
| 155 |
+ end |
|
| 156 |
+ |
|
| 157 |
+ def dependencies_missing? |
|
| 158 |
+ self.class.dependencies_missing? |
|
| 159 |
+ end |
|
| 160 |
+ |
|
| 153 | 161 |
def default_schedule |
| 154 | 162 |
self.class.default_schedule |
| 155 | 163 |
end |
@@ -317,6 +325,15 @@ class Agent < ActiveRecord::Base |
||
| 317 | 325 |
include? AgentControllerConcern |
| 318 | 326 |
end |
| 319 | 327 |
|
| 328 |
+ def gem_dependency_check |
|
| 329 |
+ @gem_dependencies_checked = true |
|
| 330 |
+ @gem_dependencies_met = yield |
|
| 331 |
+ end |
|
| 332 |
+ |
|
| 333 |
+ def dependencies_missing? |
|
| 334 |
+ @gem_dependencies_checked && !@gem_dependencies_met |
|
| 335 |
+ end |
|
| 336 |
+ |
|
| 320 | 337 |
# Find all Agents that have received Events since the last execution of this method. Update those Agents with |
| 321 | 338 |
# their new `last_checked_event_id` and queue each of the Agents to be called with #receive using `async_receive`. |
| 322 | 339 |
# This is called by bin/schedule.rb periodically. |
@@ -362,7 +379,7 @@ class Agent < ActiveRecord::Base |
||
| 362 | 379 |
def async_receive(agent_id, event_ids) |
| 363 | 380 |
agent = Agent.find(agent_id) |
| 364 | 381 |
begin |
| 365 |
- return if agent.disabled? |
|
| 382 |
+ return if agent.unavailable? |
|
| 366 | 383 |
agent.receive(Event.where(:id => event_ids)) |
| 367 | 384 |
agent.last_receive_at = Time.now |
| 368 | 385 |
agent.save! |
@@ -400,7 +417,7 @@ class Agent < ActiveRecord::Base |
||
| 400 | 417 |
def async_check(agent_id) |
| 401 | 418 |
agent = Agent.find(agent_id) |
| 402 | 419 |
begin |
| 403 |
- return if agent.disabled? |
|
| 420 |
+ return if agent.unavailable? |
|
| 404 | 421 |
agent.check |
| 405 | 422 |
agent.last_check_at = Time.now |
| 406 | 423 |
agent.save! |
@@ -1,15 +1,15 @@ |
||
| 1 |
-require 'net/ftp' |
|
| 2 |
-require 'net/ftp/list' |
|
| 3 | 1 |
require 'uri' |
| 4 | 2 |
require 'time' |
| 5 | 3 |
|
| 6 | 4 |
module Agents |
| 7 | 5 |
class FtpsiteAgent < Agent |
| 8 | 6 |
cannot_receive_events! |
| 9 |
- |
|
| 10 | 7 |
default_schedule "every_12h" |
| 11 | 8 |
|
| 9 |
+ gem_dependency_check { defined?(Net::FTP) && defined?(Net::FTP::List) }
|
|
| 10 |
+ |
|
| 12 | 11 |
description <<-MD |
| 12 |
+ #{'## Include `net-ftp-list` in your Gemfile to use this Agent!' if dependencies_missing?}
|
|
| 13 | 13 |
The FtpsiteAgent checks a FTP site and creates Events based on newly uploaded files in a directory. |
| 14 | 14 |
|
| 15 | 15 |
Specify a `url` that represents a directory of an FTP site to watch, and a list of `patterns` to match against file names. |
@@ -35,12 +35,12 @@ module Agents |
||
| 35 | 35 |
|
| 36 | 36 |
def default_options |
| 37 | 37 |
{
|
| 38 |
- 'expected_update_period_in_days' => "1", |
|
| 39 |
- 'url' => "ftp://example.org/pub/releases/", |
|
| 40 |
- 'patterns' => [ |
|
| 41 |
- 'foo-*.tar.gz', |
|
| 42 |
- ], |
|
| 43 |
- 'after' => Time.now.iso8601, |
|
| 38 |
+ 'expected_update_period_in_days' => "1", |
|
| 39 |
+ 'url' => "ftp://example.org/pub/releases/", |
|
| 40 |
+ 'patterns' => [ |
|
| 41 |
+ 'foo-*.tar.gz', |
|
| 42 |
+ ], |
|
| 43 |
+ 'after' => Time.now.iso8601, |
|
| 44 | 44 |
} |
| 45 | 45 |
end |
| 46 | 46 |
|
@@ -4,7 +4,10 @@ module Agents |
||
| 4 | 4 |
class GoogleCalendarPublishAgent < Agent |
| 5 | 5 |
cannot_be_scheduled! |
| 6 | 6 |
|
| 7 |
+ gem_dependency_check { defined?(GoogleCalendar) }
|
|
| 8 |
+ |
|
| 7 | 9 |
description <<-MD |
| 10 |
+ #{'## Include `google-api-client` in your Gemfile to use this Agent!' if dependencies_missing?}
|
|
| 8 | 11 |
The GoogleCalendarPublishAgent creates events on your google calendar. |
| 9 | 12 |
|
| 10 | 13 |
This agent relies on service accounts, rather than oauth. |
@@ -1,5 +1,3 @@ |
||
| 1 |
-require 'ruby-growl' |
|
| 2 |
- |
|
| 3 | 1 |
module Agents |
| 4 | 2 |
class GrowlAgent < Agent |
| 5 | 3 |
attr_reader :growler |
@@ -7,7 +5,10 @@ module Agents |
||
| 7 | 5 |
cannot_be_scheduled! |
| 8 | 6 |
cannot_create_events! |
| 9 | 7 |
|
| 8 |
+ gem_dependency_check { defined?(Growl) }
|
|
| 9 |
+ |
|
| 10 | 10 |
description <<-MD |
| 11 |
+ #{'## Include `ruby-growl` in your Gemfile to use this Agent!' if dependencies_missing?}
|
|
| 11 | 12 |
The GrowlAgent sends any events it receives to a Growl GNTP server immediately. |
| 12 | 13 |
|
| 13 | 14 |
It is assumed that events have a `message` or `text` key, which will hold the body of the growl notification, and a `subject` key, which will have the headline of the Growl notification. You can use Event Formatting Agent if your event does not provide these keys. |
@@ -34,13 +35,13 @@ module Agents |
||
| 34 | 35 |
errors.add(:base, "growl_server and expected_receive_period_in_days are required fields") |
| 35 | 36 |
end |
| 36 | 37 |
end |
| 37 |
- |
|
| 38 |
+ |
|
| 38 | 39 |
def register_growl |
| 39 | 40 |
@growler = Growl.new interpolated['growl_server'], interpolated['growl_app_name'], "GNTP" |
| 40 | 41 |
@growler.password = interpolated['growl_password'] |
| 41 | 42 |
@growler.add_notification interpolated['growl_notification_name'] |
| 42 | 43 |
end |
| 43 |
- |
|
| 44 |
+ |
|
| 44 | 45 |
def notify_growl(subject, message) |
| 45 | 46 |
@growler.notify(interpolated['growl_notification_name'], subject, message) |
| 46 | 47 |
end |
@@ -3,7 +3,10 @@ module Agents |
||
| 3 | 3 |
cannot_be_scheduled! |
| 4 | 4 |
cannot_create_events! |
| 5 | 5 |
|
| 6 |
+ gem_dependency_check { defined?(HipChat) }
|
|
| 7 |
+ |
|
| 6 | 8 |
description <<-MD |
| 9 |
+ #{'## Include `hipchat` in your Gemfile to use this Agent!' if dependencies_missing?}
|
|
| 7 | 10 |
The HipchatAgent sends messages to a Hipchat Room |
| 8 | 11 |
|
| 9 | 12 |
To authenticate you need to set the `auth_token`, you can get one at your Hipchat Group Admin page which you can find here: |
@@ -40,11 +43,14 @@ module Agents |
||
| 40 | 43 |
end |
| 41 | 44 |
|
| 42 | 45 |
def receive(incoming_events) |
| 43 |
- client = HipChat::Client.new(interpolated[:auth_token] || credential('hipchat_auth_token'))
|
|
| 44 | 46 |
incoming_events.each do |event| |
| 45 | 47 |
mo = interpolated(event) |
| 46 | 48 |
client[mo[:room_name]].send(mo[:username][0..14], mo[:message], :notify => boolify(mo[:notify]), :color => mo[:color]) |
| 47 | 49 |
end |
| 48 | 50 |
end |
| 51 |
+ |
|
| 52 |
+ def client |
|
| 53 |
+ @client ||= HipChat::Client.new(interpolated[:auth_token] || credential('hipchat_auth_token'))
|
|
| 54 |
+ end |
|
| 49 | 55 |
end |
| 50 | 56 |
end |
@@ -1,10 +1,11 @@ |
||
| 1 |
-require 'rturk' |
|
| 2 |
- |
|
| 3 | 1 |
module Agents |
| 4 | 2 |
class HumanTaskAgent < Agent |
| 5 | 3 |
default_schedule "every_10m" |
| 6 | 4 |
|
| 5 |
+ gem_dependency_check { defined?(RTurk) }
|
|
| 6 |
+ |
|
| 7 | 7 |
description <<-MD |
| 8 |
+ #{'## Include `rturk` in your Gemfile to use this Agent!' if dependencies_missing?}
|
|
| 8 | 9 |
You can use a HumanTaskAgent to create Human Intelligence Tasks (HITs) on Mechanical Turk. |
| 9 | 10 |
|
| 10 | 11 |
HITs can be created in response to events, or on a schedule. Set `trigger_on` to either `schedule` or `event`. |
@@ -226,266 +227,269 @@ module Agents |
||
| 226 | 227 |
|
| 227 | 228 |
protected |
| 228 | 229 |
|
| 229 |
- def take_majority? |
|
| 230 |
- interpolated['combination_mode'] == "take_majority" || interpolated['take_majority'] == "true" |
|
| 231 |
- end |
|
| 230 |
+ if defined?(RTurk) |
|
| 232 | 231 |
|
| 233 |
- def create_poll? |
|
| 234 |
- interpolated['combination_mode'] == "poll" |
|
| 235 |
- end |
|
| 232 |
+ def take_majority? |
|
| 233 |
+ interpolated['combination_mode'] == "take_majority" || interpolated['take_majority'] == "true" |
|
| 234 |
+ end |
|
| 236 | 235 |
|
| 237 |
- def event_for_hit(hit_id) |
|
| 238 |
- if memory['hits'][hit_id].is_a?(Hash) |
|
| 239 |
- Event.find_by_id(memory['hits'][hit_id]['event_id']) |
|
| 240 |
- else |
|
| 241 |
- nil |
|
| 236 |
+ def create_poll? |
|
| 237 |
+ interpolated['combination_mode'] == "poll" |
|
| 242 | 238 |
end |
| 243 |
- end |
|
| 244 | 239 |
|
| 245 |
- def hit_type(hit_id) |
|
| 246 |
- if memory['hits'][hit_id].is_a?(Hash) && memory['hits'][hit_id]['type'] |
|
| 247 |
- memory['hits'][hit_id]['type'] |
|
| 248 |
- else |
|
| 249 |
- 'user' |
|
| 240 |
+ def event_for_hit(hit_id) |
|
| 241 |
+ if memory['hits'][hit_id].is_a?(Hash) |
|
| 242 |
+ Event.find_by_id(memory['hits'][hit_id]['event_id']) |
|
| 243 |
+ else |
|
| 244 |
+ nil |
|
| 245 |
+ end |
|
| 250 | 246 |
end |
| 251 |
- end |
|
| 252 | 247 |
|
| 253 |
- def review_hits |
|
| 254 |
- reviewable_hit_ids = RTurk::GetReviewableHITs.create.hit_ids |
|
| 255 |
- my_reviewed_hit_ids = reviewable_hit_ids & (memory['hits'] || {}).keys
|
|
| 256 |
- if reviewable_hit_ids.length > 0 |
|
| 257 |
- log "MTurk reports #{reviewable_hit_ids.length} HITs, of which I own [#{my_reviewed_hit_ids.to_sentence}]"
|
|
| 248 |
+ def hit_type(hit_id) |
|
| 249 |
+ if memory['hits'][hit_id].is_a?(Hash) && memory['hits'][hit_id]['type'] |
|
| 250 |
+ memory['hits'][hit_id]['type'] |
|
| 251 |
+ else |
|
| 252 |
+ 'user' |
|
| 253 |
+ end |
|
| 258 | 254 |
end |
| 259 | 255 |
|
| 260 |
- my_reviewed_hit_ids.each do |hit_id| |
|
| 261 |
- hit = RTurk::Hit.new(hit_id) |
|
| 262 |
- assignments = hit.assignments |
|
| 256 |
+ def review_hits |
|
| 257 |
+ reviewable_hit_ids = RTurk::GetReviewableHITs.create.hit_ids |
|
| 258 |
+ my_reviewed_hit_ids = reviewable_hit_ids & (memory['hits'] || {}).keys
|
|
| 259 |
+ if reviewable_hit_ids.length > 0 |
|
| 260 |
+ log "MTurk reports #{reviewable_hit_ids.length} HITs, of which I own [#{my_reviewed_hit_ids.to_sentence}]"
|
|
| 261 |
+ end |
|
| 262 |
+ |
|
| 263 |
+ my_reviewed_hit_ids.each do |hit_id| |
|
| 264 |
+ hit = RTurk::Hit.new(hit_id) |
|
| 265 |
+ assignments = hit.assignments |
|
| 263 | 266 |
|
| 264 |
- log "Looking at HIT #{hit_id}. I found #{assignments.length} assignments#{" with the statuses: #{assignments.map(&:status).to_sentence}" if assignments.length > 0}"
|
|
| 265 |
- if assignments.length == hit.max_assignments && assignments.all? { |assignment| assignment.status == "Submitted" }
|
|
| 266 |
- inbound_event = event_for_hit(hit_id) |
|
| 267 |
+ log "Looking at HIT #{hit_id}. I found #{assignments.length} assignments#{" with the statuses: #{assignments.map(&:status).to_sentence}" if assignments.length > 0}"
|
|
| 268 |
+ if assignments.length == hit.max_assignments && assignments.all? { |assignment| assignment.status == "Submitted" }
|
|
| 269 |
+ inbound_event = event_for_hit(hit_id) |
|
| 267 | 270 |
|
| 268 |
- if hit_type(hit_id) == 'poll' |
|
| 269 |
- # handle completed polls |
|
| 271 |
+ if hit_type(hit_id) == 'poll' |
|
| 272 |
+ # handle completed polls |
|
| 270 | 273 |
|
| 271 |
- log "Handling a poll: #{hit_id}"
|
|
| 274 |
+ log "Handling a poll: #{hit_id}"
|
|
| 272 | 275 |
|
| 273 |
- scores = {}
|
|
| 274 |
- assignments.each do |assignment| |
|
| 275 |
- assignment.answers.each do |index, rating| |
|
| 276 |
- scores[index] ||= 0 |
|
| 277 |
- scores[index] += rating.to_i |
|
| 276 |
+ scores = {}
|
|
| 277 |
+ assignments.each do |assignment| |
|
| 278 |
+ assignment.answers.each do |index, rating| |
|
| 279 |
+ scores[index] ||= 0 |
|
| 280 |
+ scores[index] += rating.to_i |
|
| 281 |
+ end |
|
| 278 | 282 |
end |
| 279 |
- end |
|
| 280 | 283 |
|
| 281 |
- top_answer = scores.to_a.sort {|b, a| a.last <=> b.last }.first.first
|
|
| 284 |
+ top_answer = scores.to_a.sort {|b, a| a.last <=> b.last }.first.first
|
|
| 282 | 285 |
|
| 283 |
- payload = {
|
|
| 284 |
- 'answers' => memory['hits'][hit_id]['answers'], |
|
| 285 |
- 'poll' => assignments.map(&:answers), |
|
| 286 |
- 'best_answer' => memory['hits'][hit_id]['answers'][top_answer.to_i - 1] |
|
| 287 |
- } |
|
| 286 |
+ payload = {
|
|
| 287 |
+ 'answers' => memory['hits'][hit_id]['answers'], |
|
| 288 |
+ 'poll' => assignments.map(&:answers), |
|
| 289 |
+ 'best_answer' => memory['hits'][hit_id]['answers'][top_answer.to_i - 1] |
|
| 290 |
+ } |
|
| 288 | 291 |
|
| 289 |
- event = create_event :payload => payload |
|
| 290 |
- log "Event emitted with answer(s) for poll", :outbound_event => event, :inbound_event => inbound_event |
|
| 291 |
- else |
|
| 292 |
- # handle normal completed HITs |
|
| 293 |
- payload = { 'answers' => assignments.map(&:answers) }
|
|
| 294 |
- |
|
| 295 |
- if take_majority? |
|
| 296 |
- counts = {}
|
|
| 297 |
- options['hit']['questions'].each do |question| |
|
| 298 |
- question_counts = question['selections'].inject({}) { |memo, selection| memo[selection['key']] = 0; memo }
|
|
| 299 |
- assignments.each do |assignment| |
|
| 300 |
- answers = ActiveSupport::HashWithIndifferentAccess.new(assignment.answers) |
|
| 301 |
- answer = answers[question['key']] |
|
| 302 |
- question_counts[answer] += 1 |
|
| 292 |
+ event = create_event :payload => payload |
|
| 293 |
+ log "Event emitted with answer(s) for poll", :outbound_event => event, :inbound_event => inbound_event |
|
| 294 |
+ else |
|
| 295 |
+ # handle normal completed HITs |
|
| 296 |
+ payload = { 'answers' => assignments.map(&:answers) }
|
|
| 297 |
+ |
|
| 298 |
+ if take_majority? |
|
| 299 |
+ counts = {}
|
|
| 300 |
+ options['hit']['questions'].each do |question| |
|
| 301 |
+ question_counts = question['selections'].inject({}) { |memo, selection| memo[selection['key']] = 0; memo }
|
|
| 302 |
+ assignments.each do |assignment| |
|
| 303 |
+ answers = ActiveSupport::HashWithIndifferentAccess.new(assignment.answers) |
|
| 304 |
+ answer = answers[question['key']] |
|
| 305 |
+ question_counts[answer] += 1 |
|
| 306 |
+ end |
|
| 307 |
+ counts[question['key']] = question_counts |
|
| 303 | 308 |
end |
| 304 |
- counts[question['key']] = question_counts |
|
| 305 |
- end |
|
| 306 |
- payload['counts'] = counts |
|
| 309 |
+ payload['counts'] = counts |
|
| 307 | 310 |
|
| 308 |
- majority_answer = counts.inject({}) do |memo, (key, question_counts)|
|
|
| 309 |
- memo[key] = question_counts.to_a.sort {|a, b| a.last <=> b.last }.last.first
|
|
| 310 |
- memo |
|
| 311 |
- end |
|
| 312 |
- payload['majority_answer'] = majority_answer |
|
| 313 |
- |
|
| 314 |
- if all_questions_are_numeric? |
|
| 315 |
- average_answer = counts.inject({}) do |memo, (key, question_counts)|
|
|
| 316 |
- sum = divisor = 0 |
|
| 317 |
- question_counts.to_a.each do |num, count| |
|
| 318 |
- sum += num.to_s.to_f * count |
|
| 319 |
- divisor += count |
|
| 320 |
- end |
|
| 321 |
- memo[key] = sum / divisor.to_f |
|
| 311 |
+ majority_answer = counts.inject({}) do |memo, (key, question_counts)|
|
|
| 312 |
+ memo[key] = question_counts.to_a.sort {|a, b| a.last <=> b.last }.last.first
|
|
| 322 | 313 |
memo |
| 323 | 314 |
end |
| 324 |
- payload['average_answer'] = average_answer |
|
| 325 |
- end |
|
| 326 |
- end |
|
| 327 |
- |
|
| 328 |
- if create_poll? |
|
| 329 |
- questions = [] |
|
| 330 |
- selections = 5.times.map { |i| { 'key' => i+1, 'text' => i+1 } }.reverse
|
|
| 331 |
- assignments.length.times do |index| |
|
| 332 |
- questions << {
|
|
| 333 |
- 'type' => "selection", |
|
| 334 |
- 'name' => "Item #{index + 1}",
|
|
| 335 |
- 'key' => index, |
|
| 336 |
- 'required' => "true", |
|
| 337 |
- 'question' => interpolate_string(options['poll_options']['row_template'], assignments[index].answers), |
|
| 338 |
- 'selections' => selections |
|
| 339 |
- } |
|
| 315 |
+ payload['majority_answer'] = majority_answer |
|
| 316 |
+ |
|
| 317 |
+ if all_questions_are_numeric? |
|
| 318 |
+ average_answer = counts.inject({}) do |memo, (key, question_counts)|
|
|
| 319 |
+ sum = divisor = 0 |
|
| 320 |
+ question_counts.to_a.each do |num, count| |
|
| 321 |
+ sum += num.to_s.to_f * count |
|
| 322 |
+ divisor += count |
|
| 323 |
+ end |
|
| 324 |
+ memo[key] = sum / divisor.to_f |
|
| 325 |
+ memo |
|
| 326 |
+ end |
|
| 327 |
+ payload['average_answer'] = average_answer |
|
| 328 |
+ end |
|
| 340 | 329 |
end |
| 341 | 330 |
|
| 342 |
- poll_hit = create_hit 'title' => options['poll_options']['title'], |
|
| 343 |
- 'description' => options['poll_options']['instructions'], |
|
| 344 |
- 'questions' => questions, |
|
| 345 |
- 'assignments' => options['poll_options']['assignments'], |
|
| 346 |
- 'lifetime_in_seconds' => options['poll_options']['lifetime_in_seconds'], |
|
| 347 |
- 'reward' => options['poll_options']['reward'], |
|
| 348 |
- 'payload' => inbound_event && inbound_event.payload, |
|
| 349 |
- 'metadata' => { 'type' => 'poll',
|
|
| 350 |
- 'original_hit' => hit_id, |
|
| 351 |
- 'answers' => assignments.map(&:answers), |
|
| 352 |
- 'event_id' => inbound_event && inbound_event.id } |
|
| 353 |
- |
|
| 354 |
- log "Poll HIT created with ID #{poll_hit.id} and URL #{poll_hit.url}. Original HIT: #{hit_id}", :inbound_event => inbound_event
|
|
| 355 |
- else |
|
| 356 |
- if options[:separate_answers] |
|
| 357 |
- payload['answers'].each.with_index do |answer, index| |
|
| 358 |
- sub_payload = payload.dup |
|
| 359 |
- sub_payload.delete('answers')
|
|
| 360 |
- sub_payload['answer'] = answer |
|
| 361 |
- event = create_event :payload => sub_payload |
|
| 362 |
- log "Event emitted with answer ##{index}", :outbound_event => event, :inbound_event => inbound_event
|
|
| 331 |
+ if create_poll? |
|
| 332 |
+ questions = [] |
|
| 333 |
+ selections = 5.times.map { |i| { 'key' => i+1, 'text' => i+1 } }.reverse
|
|
| 334 |
+ assignments.length.times do |index| |
|
| 335 |
+ questions << {
|
|
| 336 |
+ 'type' => "selection", |
|
| 337 |
+ 'name' => "Item #{index + 1}",
|
|
| 338 |
+ 'key' => index, |
|
| 339 |
+ 'required' => "true", |
|
| 340 |
+ 'question' => interpolate_string(options['poll_options']['row_template'], assignments[index].answers), |
|
| 341 |
+ 'selections' => selections |
|
| 342 |
+ } |
|
| 363 | 343 |
end |
| 344 |
+ |
|
| 345 |
+ poll_hit = create_hit 'title' => options['poll_options']['title'], |
|
| 346 |
+ 'description' => options['poll_options']['instructions'], |
|
| 347 |
+ 'questions' => questions, |
|
| 348 |
+ 'assignments' => options['poll_options']['assignments'], |
|
| 349 |
+ 'lifetime_in_seconds' => options['poll_options']['lifetime_in_seconds'], |
|
| 350 |
+ 'reward' => options['poll_options']['reward'], |
|
| 351 |
+ 'payload' => inbound_event && inbound_event.payload, |
|
| 352 |
+ 'metadata' => { 'type' => 'poll',
|
|
| 353 |
+ 'original_hit' => hit_id, |
|
| 354 |
+ 'answers' => assignments.map(&:answers), |
|
| 355 |
+ 'event_id' => inbound_event && inbound_event.id } |
|
| 356 |
+ |
|
| 357 |
+ log "Poll HIT created with ID #{poll_hit.id} and URL #{poll_hit.url}. Original HIT: #{hit_id}", :inbound_event => inbound_event
|
|
| 364 | 358 |
else |
| 365 |
- event = create_event :payload => payload |
|
| 366 |
- log "Event emitted with answer(s)", :outbound_event => event, :inbound_event => inbound_event |
|
| 359 |
+ if options[:separate_answers] |
|
| 360 |
+ payload['answers'].each.with_index do |answer, index| |
|
| 361 |
+ sub_payload = payload.dup |
|
| 362 |
+ sub_payload.delete('answers')
|
|
| 363 |
+ sub_payload['answer'] = answer |
|
| 364 |
+ event = create_event :payload => sub_payload |
|
| 365 |
+ log "Event emitted with answer ##{index}", :outbound_event => event, :inbound_event => inbound_event
|
|
| 366 |
+ end |
|
| 367 |
+ else |
|
| 368 |
+ event = create_event :payload => payload |
|
| 369 |
+ log "Event emitted with answer(s)", :outbound_event => event, :inbound_event => inbound_event |
|
| 370 |
+ end |
|
| 367 | 371 |
end |
| 368 | 372 |
end |
| 369 |
- end |
|
| 370 | 373 |
|
| 371 |
- assignments.each(&:approve!) |
|
| 372 |
- hit.dispose! |
|
| 374 |
+ assignments.each(&:approve!) |
|
| 375 |
+ hit.dispose! |
|
| 373 | 376 |
|
| 374 |
- memory['hits'].delete(hit_id) |
|
| 377 |
+ memory['hits'].delete(hit_id) |
|
| 378 |
+ end |
|
| 375 | 379 |
end |
| 376 | 380 |
end |
| 377 |
- end |
|
| 378 | 381 |
|
| 379 |
- def all_questions_are_numeric? |
|
| 380 |
- interpolated['hit']['questions'].all? do |question| |
|
| 381 |
- question['selections'].all? do |selection| |
|
| 382 |
- selection['key'] == selection['key'].to_f.to_s || selection['key'] == selection['key'].to_i.to_s |
|
| 382 |
+ def all_questions_are_numeric? |
|
| 383 |
+ interpolated['hit']['questions'].all? do |question| |
|
| 384 |
+ question['selections'].all? do |selection| |
|
| 385 |
+ selection['key'] == selection['key'].to_f.to_s || selection['key'] == selection['key'].to_i.to_s |
|
| 386 |
+ end |
|
| 383 | 387 |
end |
| 384 | 388 |
end |
| 385 |
- end |
|
| 386 |
- |
|
| 387 |
- def create_basic_hit(event = nil) |
|
| 388 |
- hit = create_hit 'title' => options['hit']['title'], |
|
| 389 |
- 'description' => options['hit']['description'], |
|
| 390 |
- 'questions' => options['hit']['questions'], |
|
| 391 |
- 'assignments' => options['hit']['assignments'], |
|
| 392 |
- 'lifetime_in_seconds' => options['hit']['lifetime_in_seconds'], |
|
| 393 |
- 'reward' => options['hit']['reward'], |
|
| 394 |
- 'payload' => event && event.payload, |
|
| 395 |
- 'metadata' => { 'event_id' => event && event.id }
|
|
| 396 |
- |
|
| 397 |
- log "HIT created with ID #{hit.id} and URL #{hit.url}", :inbound_event => event
|
|
| 398 |
- end |
|
| 399 | 389 |
|
| 400 |
- def create_hit(opts = {})
|
|
| 401 |
- payload = opts['payload'] || {}
|
|
| 402 |
- title = interpolate_string(opts['title'], payload).strip |
|
| 403 |
- description = interpolate_string(opts['description'], payload).strip |
|
| 404 |
- questions = interpolate_options(opts['questions'], payload) |
|
| 405 |
- hit = RTurk::Hit.create(:title => title) do |hit| |
|
| 406 |
- hit.max_assignments = (opts['assignments'] || 1).to_i |
|
| 407 |
- hit.description = description |
|
| 408 |
- hit.lifetime = (opts['lifetime_in_seconds'] || 24 * 60 * 60).to_i |
|
| 409 |
- hit.question_form AgentQuestionForm.new(:title => title, :description => description, :questions => questions) |
|
| 410 |
- hit.reward = (opts['reward'] || 0.05).to_f |
|
| 411 |
- #hit.qualifications.add :approval_rate, { :gt => 80 }
|
|
| 390 |
+ def create_basic_hit(event = nil) |
|
| 391 |
+ hit = create_hit 'title' => options['hit']['title'], |
|
| 392 |
+ 'description' => options['hit']['description'], |
|
| 393 |
+ 'questions' => options['hit']['questions'], |
|
| 394 |
+ 'assignments' => options['hit']['assignments'], |
|
| 395 |
+ 'lifetime_in_seconds' => options['hit']['lifetime_in_seconds'], |
|
| 396 |
+ 'reward' => options['hit']['reward'], |
|
| 397 |
+ 'payload' => event && event.payload, |
|
| 398 |
+ 'metadata' => { 'event_id' => event && event.id }
|
|
| 399 |
+ |
|
| 400 |
+ log "HIT created with ID #{hit.id} and URL #{hit.url}", :inbound_event => event
|
|
| 412 | 401 |
end |
| 413 |
- memory['hits'] ||= {}
|
|
| 414 |
- memory['hits'][hit.id] = opts['metadata'] || {}
|
|
| 415 |
- hit |
|
| 416 |
- end |
|
| 417 | 402 |
|
| 418 |
- # RTurk Question Form |
|
| 403 |
+ def create_hit(opts = {})
|
|
| 404 |
+ payload = opts['payload'] || {}
|
|
| 405 |
+ title = interpolate_string(opts['title'], payload).strip |
|
| 406 |
+ description = interpolate_string(opts['description'], payload).strip |
|
| 407 |
+ questions = interpolate_options(opts['questions'], payload) |
|
| 408 |
+ hit = RTurk::Hit.create(:title => title) do |hit| |
|
| 409 |
+ hit.max_assignments = (opts['assignments'] || 1).to_i |
|
| 410 |
+ hit.description = description |
|
| 411 |
+ hit.lifetime = (opts['lifetime_in_seconds'] || 24 * 60 * 60).to_i |
|
| 412 |
+ hit.question_form AgentQuestionForm.new(:title => title, :description => description, :questions => questions) |
|
| 413 |
+ hit.reward = (opts['reward'] || 0.05).to_f |
|
| 414 |
+ #hit.qualifications.add :approval_rate, { :gt => 80 }
|
|
| 415 |
+ end |
|
| 416 |
+ memory['hits'] ||= {}
|
|
| 417 |
+ memory['hits'][hit.id] = opts['metadata'] || {}
|
|
| 418 |
+ hit |
|
| 419 |
+ end |
|
| 419 | 420 |
|
| 420 |
- class AgentQuestionForm < RTurk::QuestionForm |
|
| 421 |
- needs :title, :description, :questions |
|
| 421 |
+ # RTurk Question Form |
|
| 422 | 422 |
|
| 423 |
- def question_form_content |
|
| 424 |
- Overview do |
|
| 425 |
- Title do |
|
| 426 |
- text @title |
|
| 427 |
- end |
|
| 428 |
- Text do |
|
| 429 |
- text @description |
|
| 430 |
- end |
|
| 431 |
- end |
|
| 423 |
+ class AgentQuestionForm < RTurk::QuestionForm |
|
| 424 |
+ needs :title, :description, :questions |
|
| 432 | 425 |
|
| 433 |
- @questions.each.with_index do |question, index| |
|
| 434 |
- Question do |
|
| 435 |
- QuestionIdentifier do |
|
| 436 |
- text question['key'] || "question_#{index}"
|
|
| 426 |
+ def question_form_content |
|
| 427 |
+ Overview do |
|
| 428 |
+ Title do |
|
| 429 |
+ text @title |
|
| 437 | 430 |
end |
| 438 |
- DisplayName do |
|
| 439 |
- text question['name'] || "Question ##{index}"
|
|
| 431 |
+ Text do |
|
| 432 |
+ text @description |
|
| 440 | 433 |
end |
| 441 |
- IsRequired do |
|
| 442 |
- text question['required'] || 'true' |
|
| 443 |
- end |
|
| 444 |
- QuestionContent do |
|
| 445 |
- Text do |
|
| 446 |
- text question['question'] |
|
| 434 |
+ end |
|
| 435 |
+ |
|
| 436 |
+ @questions.each.with_index do |question, index| |
|
| 437 |
+ Question do |
|
| 438 |
+ QuestionIdentifier do |
|
| 439 |
+ text question['key'] || "question_#{index}"
|
|
| 447 | 440 |
end |
| 448 |
- end |
|
| 449 |
- AnswerSpecification do |
|
| 450 |
- if question['type'] == "selection" |
|
| 441 |
+ DisplayName do |
|
| 442 |
+ text question['name'] || "Question ##{index}"
|
|
| 443 |
+ end |
|
| 444 |
+ IsRequired do |
|
| 445 |
+ text question['required'] || 'true' |
|
| 446 |
+ end |
|
| 447 |
+ QuestionContent do |
|
| 448 |
+ Text do |
|
| 449 |
+ text question['question'] |
|
| 450 |
+ end |
|
| 451 |
+ end |
|
| 452 |
+ AnswerSpecification do |
|
| 453 |
+ if question['type'] == "selection" |
|
| 451 | 454 |
|
| 452 |
- SelectionAnswer do |
|
| 453 |
- StyleSuggestion do |
|
| 454 |
- text 'radiobutton' |
|
| 455 |
- end |
|
| 456 |
- Selections do |
|
| 457 |
- question['selections'].each do |selection| |
|
| 458 |
- Selection do |
|
| 459 |
- SelectionIdentifier do |
|
| 460 |
- text selection['key'] |
|
| 461 |
- end |
|
| 462 |
- Text do |
|
| 463 |
- text selection['text'] |
|
| 455 |
+ SelectionAnswer do |
|
| 456 |
+ StyleSuggestion do |
|
| 457 |
+ text 'radiobutton' |
|
| 458 |
+ end |
|
| 459 |
+ Selections do |
|
| 460 |
+ question['selections'].each do |selection| |
|
| 461 |
+ Selection do |
|
| 462 |
+ SelectionIdentifier do |
|
| 463 |
+ text selection['key'] |
|
| 464 |
+ end |
|
| 465 |
+ Text do |
|
| 466 |
+ text selection['text'] |
|
| 467 |
+ end |
|
| 464 | 468 |
end |
| 465 | 469 |
end |
| 466 | 470 |
end |
| 467 | 471 |
end |
| 468 |
- end |
|
| 469 | 472 |
|
| 470 |
- else |
|
| 473 |
+ else |
|
| 471 | 474 |
|
| 472 |
- FreeTextAnswer do |
|
| 473 |
- if question['min_length'].present? || question['max_length'].present? |
|
| 474 |
- Constraints do |
|
| 475 |
- lengths = {}
|
|
| 476 |
- lengths['minLength'] = question['min_length'].to_s if question['min_length'].present? |
|
| 477 |
- lengths['maxLength'] = question['max_length'].to_s if question['max_length'].present? |
|
| 478 |
- Length lengths |
|
| 475 |
+ FreeTextAnswer do |
|
| 476 |
+ if question['min_length'].present? || question['max_length'].present? |
|
| 477 |
+ Constraints do |
|
| 478 |
+ lengths = {}
|
|
| 479 |
+ lengths['minLength'] = question['min_length'].to_s if question['min_length'].present? |
|
| 480 |
+ lengths['maxLength'] = question['max_length'].to_s if question['max_length'].present? |
|
| 481 |
+ Length lengths |
|
| 482 |
+ end |
|
| 479 | 483 |
end |
| 480 |
- end |
|
| 481 | 484 |
|
| 482 |
- if question['default'].present? |
|
| 483 |
- DefaultText do |
|
| 484 |
- text question['default'] |
|
| 485 |
+ if question['default'].present? |
|
| 486 |
+ DefaultText do |
|
| 487 |
+ text question['default'] |
|
| 488 |
+ end |
|
| 485 | 489 |
end |
| 486 | 490 |
end |
| 487 |
- end |
|
| 488 | 491 |
|
| 492 |
+ end |
|
| 489 | 493 |
end |
| 490 | 494 |
end |
| 491 | 495 |
end |
@@ -3,7 +3,10 @@ module Agents |
||
| 3 | 3 |
cannot_be_scheduled! |
| 4 | 4 |
cannot_create_events! |
| 5 | 5 |
|
| 6 |
+ gem_dependency_check { defined?(Jabber) }
|
|
| 7 |
+ |
|
| 6 | 8 |
description <<-MD |
| 9 |
+ #{'## Include `xmpp4r` in your Gemfile to use this Agent!' if dependencies_missing?}
|
|
| 7 | 10 |
The JabberAgent will send any events it receives to your Jabber/XMPP IM account. |
| 8 | 11 |
|
| 9 | 12 |
Specify the `jabber_server` and `jabber_port` for your Jabber server. |
@@ -1,10 +1,12 @@ |
||
| 1 | 1 |
# encoding: utf-8 |
| 2 |
-require "mqtt" |
|
| 3 | 2 |
require "json" |
| 4 | 3 |
|
| 5 | 4 |
module Agents |
| 6 | 5 |
class MqttAgent < Agent |
| 6 |
+ gem_dependency_check { defined?(MQTT) }
|
|
| 7 |
+ |
|
| 7 | 8 |
description <<-MD |
| 9 |
+ #{'## Include `mqtt` in your Gemfile to use this Agent!' if dependencies_missing?}
|
|
| 8 | 10 |
The MQTT agent allows both publication and subscription to an MQTT topic. |
| 9 | 11 |
|
| 10 | 12 |
MQTT is a generic transport protocol for machine to machine communication. |
@@ -1,11 +1,15 @@ |
||
| 1 | 1 |
module Agents |
| 2 | 2 |
class SlackAgent < Agent |
| 3 |
+ DEFAULT_WEBHOOK = 'incoming-webhook' |
|
| 4 |
+ DEFAULT_USERNAME = 'Huginn' |
|
| 5 |
+ |
|
| 3 | 6 |
cannot_be_scheduled! |
| 4 | 7 |
cannot_create_events! |
| 5 | 8 |
|
| 6 |
- DEFAULT_WEBHOOK = 'incoming-webhook' |
|
| 7 |
- DEFAULT_USERNAME = 'Huginn' |
|
| 9 |
+ gem_dependency_check { defined?(Slack) }
|
|
| 10 |
+ |
|
| 8 | 11 |
description <<-MD |
| 12 |
+ #{'## Include `slack-notifier` in your Gemfile to use this Agent!' if dependencies_missing?}
|
|
| 9 | 13 |
The SlackAgent lets you receive events and send notifications to [slack](https://slack.com/). |
| 10 | 14 |
|
| 11 | 15 |
To get started, you will first need to setup an incoming webhook. |
@@ -1,4 +1,3 @@ |
||
| 1 |
-require 'twilio-ruby' |
|
| 2 | 1 |
require 'securerandom' |
| 3 | 2 |
|
| 4 | 3 |
module Agents |
@@ -6,7 +5,10 @@ module Agents |
||
| 6 | 5 |
cannot_be_scheduled! |
| 7 | 6 |
cannot_create_events! |
| 8 | 7 |
|
| 8 |
+ gem_dependency_check { defined?(Twilio) }
|
|
| 9 |
+ |
|
| 9 | 10 |
description <<-MD |
| 11 |
+ #{'## Include `twilio-ruby` in your Gemfile to use this Agent!' if dependencies_missing?}
|
|
| 10 | 12 |
The TwilioAgent receives and collects events and sends them via text message (up to 160 characters) or gives you a call when scheduled. |
| 11 | 13 |
|
| 12 | 14 |
It is assumed that events have a `message`, `text`, or `sms` key, the value of which is sent as the content of the text message/call. You can use the EventFormattingAgent if your event does not provide these keys. |
@@ -39,7 +41,6 @@ module Agents |
||
| 39 | 41 |
end |
| 40 | 42 |
|
| 41 | 43 |
def receive(incoming_events) |
| 42 |
- @client = Twilio::REST::Client.new interpolated['account_sid'], interpolated['auth_token'] |
|
| 43 | 44 |
memory['pending_calls'] ||= {}
|
| 44 | 45 |
incoming_events.each do |event| |
| 45 | 46 |
message = (event.payload['message'].presence || event.payload['text'].presence || event.payload['sms'].presence).to_s |
@@ -63,15 +64,15 @@ module Agents |
||
| 63 | 64 |
end |
| 64 | 65 |
|
| 65 | 66 |
def send_message(message) |
| 66 |
- @client.account.sms.messages.create :from => interpolated['sender_cell'], |
|
| 67 |
- :to => interpolated['receiver_cell'], |
|
| 68 |
- :body => message |
|
| 67 |
+ client.account.sms.messages.create :from => interpolated['sender_cell'], |
|
| 68 |
+ :to => interpolated['receiver_cell'], |
|
| 69 |
+ :body => message |
|
| 69 | 70 |
end |
| 70 | 71 |
|
| 71 | 72 |
def make_call(secret) |
| 72 |
- @client.account.calls.create :from => interpolated['sender_cell'], |
|
| 73 |
- :to => interpolated['receiver_cell'], |
|
| 74 |
- :url => post_url(interpolated['server_url'], secret) |
|
| 73 |
+ client.account.calls.create :from => interpolated['sender_cell'], |
|
| 74 |
+ :to => interpolated['receiver_cell'], |
|
| 75 |
+ :url => post_url(interpolated['server_url'], secret) |
|
| 75 | 76 |
end |
| 76 | 77 |
|
| 77 | 78 |
def post_url(server_url, secret) |
@@ -85,5 +86,9 @@ module Agents |
||
| 85 | 86 |
[response.text, 200] |
| 86 | 87 |
end |
| 87 | 88 |
end |
| 89 |
+ |
|
| 90 |
+ def client |
|
| 91 |
+ @client ||= Twilio::REST::Client.new interpolated['account_sid'], interpolated['auth_token'] |
|
| 92 |
+ end |
|
| 88 | 93 |
end |
| 89 | 94 |
end |
@@ -5,7 +5,10 @@ module Agents |
||
| 5 | 5 |
class WeatherAgent < Agent |
| 6 | 6 |
cannot_receive_events! |
| 7 | 7 |
|
| 8 |
+ gem_dependency_check { defined?(Wunderground) && defined?(ForecastIO) }
|
|
| 9 |
+ |
|
| 8 | 10 |
description <<-MD |
| 11 |
+ #{'## Include `forecast_io` and `wunderground` in your Gemfile to use this Agent!' if dependencies_missing?}
|
|
| 9 | 12 |
The WeatherAgent creates an event for the day's weather at a given `location`. |
| 10 | 13 |
|
| 11 | 14 |
You also must select `which_day` you would like to get the weather for where the number 0 is for today and 1 is for tomorrow and so on. Weather is only returned for 1 week at a time. |
@@ -1,5 +1,4 @@ |
||
| 1 | 1 |
# encoding: utf-8 |
| 2 |
-require "weibo_2" |
|
| 3 | 2 |
|
| 4 | 3 |
module Agents |
| 5 | 4 |
class WeiboPublishAgent < Agent |
@@ -8,6 +7,7 @@ module Agents |
||
| 8 | 7 |
cannot_be_scheduled! |
| 9 | 8 |
|
| 10 | 9 |
description <<-MD |
| 10 |
+ #{'## Include `weibo_2` in your Gemfile to use this Agent!' if dependencies_missing?}
|
|
| 11 | 11 |
The WeiboPublishAgent publishes tweets from the events it receives. |
| 12 | 12 |
|
| 13 | 13 |
You must first set up a Weibo app and generate an `acess_token` for the user to send statuses as. |
@@ -79,8 +79,7 @@ module Agents |
||
| 79 | 79 |
tweet_json[:entities][:urls].each do |url| |
| 80 | 80 |
text.gsub! url[:url], url[:expanded_url] |
| 81 | 81 |
end |
| 82 |
- return text |
|
| 82 |
+ text |
|
| 83 | 83 |
end |
| 84 |
- |
|
| 85 | 84 |
end |
| 86 | 85 |
end |
@@ -1,5 +1,4 @@ |
||
| 1 | 1 |
# encoding: utf-8 |
| 2 |
-require "weibo_2" |
|
| 3 | 2 |
|
| 4 | 3 |
module Agents |
| 5 | 4 |
class WeiboUserAgent < Agent |
@@ -8,6 +7,7 @@ module Agents |
||
| 8 | 7 |
cannot_receive_events! |
| 9 | 8 |
|
| 10 | 9 |
description <<-MD |
| 10 |
+ #{'## Include `weibo_2` in your Gemfile to use this Agent!' if dependencies_missing?}
|
|
| 11 | 11 |
The WeiboUserAgent follows the timeline of a specified Weibo user. It uses this endpoint: http://open.weibo.com/wiki/2/statuses/user_timeline/en |
| 12 | 12 |
|
| 13 | 13 |
You must first set up a Weibo app and generate an `acess_token` to authenticate with. Provide that, along with the `app_key` and `app_secret` for your Weibo app in the options. |
@@ -13,7 +13,7 @@ |
||
| 13 | 13 |
|
| 14 | 14 |
<% @agents.each do |agent| %> |
| 15 | 15 |
<tr> |
| 16 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
| 16 |
+ <td class='<%= "agent-unavailable" if agent.unavailable? %>'> |
|
| 17 | 17 |
<%= link_to agent.name, agent_path(agent) %> |
| 18 | 18 |
<br/> |
| 19 | 19 |
<span class='text-muted'><%= agent.short_type.titleize %></span> |
@@ -23,35 +23,35 @@ |
||
| 23 | 23 |
</span> |
| 24 | 24 |
<% end %> |
| 25 | 25 |
</td> |
| 26 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
| 26 |
+ <td class='<%= "agent-unavailable" if agent.unavailable? %>'> |
|
| 27 | 27 |
<% if agent.can_be_scheduled? %> |
| 28 | 28 |
<%= agent_schedule(agent, ',<br/>') %> |
| 29 | 29 |
<% else %> |
| 30 | 30 |
<span class='not-applicable'></span> |
| 31 | 31 |
<% end %> |
| 32 | 32 |
</td> |
| 33 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
| 33 |
+ <td class='<%= "agent-unavailable" if agent.unavailable? %>'> |
|
| 34 | 34 |
<% if agent.can_be_scheduled? %> |
| 35 | 35 |
<%= agent.last_check_at ? time_ago_in_words(agent.last_check_at) + " ago" : "never" %> |
| 36 | 36 |
<% else %> |
| 37 | 37 |
<span class='not-applicable'></span> |
| 38 | 38 |
<% end %> |
| 39 | 39 |
</td> |
| 40 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
| 40 |
+ <td class='<%= "agent-unavailable" if agent.unavailable? %>'> |
|
| 41 | 41 |
<% if agent.can_create_events? %> |
| 42 | 42 |
<%= agent.last_event_at ? time_ago_in_words(agent.last_event_at) + " ago" : "never" %> |
| 43 | 43 |
<% else %> |
| 44 | 44 |
<span class='not-applicable'></span> |
| 45 | 45 |
<% end %> |
| 46 | 46 |
</td> |
| 47 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
| 47 |
+ <td class='<%= "agent-unavailable" if agent.unavailable? %>'> |
|
| 48 | 48 |
<% if agent.can_receive_events? %> |
| 49 | 49 |
<%= agent.last_receive_at ? time_ago_in_words(agent.last_receive_at) + " ago" : "never" %> |
| 50 | 50 |
<% else %> |
| 51 | 51 |
<span class='not-applicable'></span> |
| 52 | 52 |
<% end %> |
| 53 | 53 |
</td> |
| 54 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
| 54 |
+ <td class='<%= "agent-unavailable" if agent.unavailable? %>'> |
|
| 55 | 55 |
<% if agent.can_create_events? %> |
| 56 | 56 |
<%= link_to(agent.events_count || 0, agent_events_path(agent)) %> |
| 57 | 57 |
<% else %> |
@@ -11,13 +11,13 @@ |
||
| 11 | 11 |
<%= link_to 'wiki', 'https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications', target: :_blank %> |
| 12 | 12 |
for guidance. |
| 13 | 13 |
</p> |
| 14 |
- <% if has_oauth_configuration_for('twitter') %>
|
|
| 14 |
+ <% if has_oauth_configuration_for?('twitter') %>
|
|
| 15 | 15 |
<p><%= link_to "Authenticate with Twitter", "/auth/twitter" %></p> |
| 16 | 16 |
<% end %> |
| 17 |
- <% if has_oauth_configuration_for('thirty_seven_signals') %>
|
|
| 17 |
+ <% if has_oauth_configuration_for?('thirty_seven_signals') %>
|
|
| 18 | 18 |
<p><%= link_to "Authenticate with 37Signals (Basecamp)", "/auth/37signals" %></p> |
| 19 | 19 |
<% end -%> |
| 20 |
- <% if has_oauth_configuration_for('github') %>
|
|
| 20 |
+ <% if has_oauth_configuration_for?('github') %>
|
|
| 21 | 21 |
<p><%= link_to "Authenticate with Github", "/auth/github" %></p> |
| 22 | 22 |
<% end -%> |
| 23 | 23 |
<hr> |
@@ -1,4 +1,4 @@ |
||
| 1 |
-unless Rails.env.test? |
|
| 1 |
+if defined?(RTurk) && !Rails.env.test? |
|
| 2 | 2 |
RTurk::logger.level = Logger::DEBUG |
| 3 | 3 |
RTurk.setup(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_ACCESS_KEY'], :sandbox => ENV['AWS_SANDBOX'] == "true") |
| 4 | 4 |
end |
@@ -1,5 +1,23 @@ |
||
| 1 |
+LOADED_OMNIAUTH_STRATEGIES = {
|
|
| 2 |
+ 'twitter' => defined?(OmniAuth::Strategies::Twitter), |
|
| 3 |
+ '37signals' => defined?(OmniAuth::Strategies::ThirtySevenSignals), |
|
| 4 |
+ 'github' => defined?(OmniAuth::Strategies::GitHub) |
|
| 5 |
+} |
|
| 6 |
+ |
|
| 7 |
+def has_oauth_configuration_for?(provider) |
|
| 8 |
+ LOADED_OMNIAUTH_STRATEGIES[provider.to_s] && ENV["#{provider.upcase}_OAUTH_KEY"].present? && ENV["#{provider.upcase}_OAUTH_SECRET"].present?
|
|
| 9 |
+end |
|
| 10 |
+ |
|
| 1 | 11 |
Rails.application.config.middleware.use OmniAuth::Builder do |
| 2 |
- provider :twitter, ENV['TWITTER_OAUTH_KEY'], ENV['TWITTER_OAUTH_SECRET'], authorize_params: {force_login: 'true', use_authorize: 'true'}
|
|
| 3 |
- provider '37signals', ENV['THIRTY_SEVEN_SIGNALS_OAUTH_KEY'], ENV['THIRTY_SEVEN_SIGNALS_OAUTH_SECRET'] |
|
| 4 |
- provider :github, ENV['GITHUB_OAUTH_KEY'], ENV['GITHUB_OAUTH_SECRET'] |
|
| 12 |
+ if has_oauth_configuration_for?('twitter')
|
|
| 13 |
+ provider 'twitter', ENV['TWITTER_OAUTH_KEY'], ENV['TWITTER_OAUTH_SECRET'], authorize_params: {force_login: 'true', use_authorize: 'true'}
|
|
| 14 |
+ end |
|
| 15 |
+ |
|
| 16 |
+ if has_oauth_configuration_for?('37signals')
|
|
| 17 |
+ provider '37signals', ENV['THIRTY_SEVEN_SIGNALS_OAUTH_KEY'], ENV['THIRTY_SEVEN_SIGNALS_OAUTH_SECRET'] |
|
| 18 |
+ end |
|
| 19 |
+ |
|
| 20 |
+ if has_oauth_configuration_for?('github')
|
|
| 21 |
+ provider 'github', ENV['GITHUB_OAUTH_KEY'], ENV['GITHUB_OAUTH_SECRET'] |
|
| 22 |
+ end |
|
| 5 | 23 |
end |
@@ -40,7 +40,7 @@ class Rufus::Scheduler |
||
| 40 | 40 |
def schedule_scheduler_agent(agent) |
| 41 | 41 |
job = scheduler_agent_job(agent) |
| 42 | 42 |
|
| 43 |
- if agent.disabled? |
|
| 43 |
+ if agent.unavailable? |
|
| 44 | 44 |
if job |
| 45 | 45 |
puts "Unscheduling SchedulerAgent##{agent.id} (disabled)"
|
| 46 | 46 |
job.unschedule |