@@ -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,67 @@ 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 'jquery-rails', '~> 3.1.0' |
|
50 |
+gem 'json', '~> 1.8.1' |
|
51 |
+gem 'jsonpath', '~> 0.5.6' |
|
52 |
+gem 'kaminari', '~> 0.16.1' |
|
64 | 53 |
gem 'kramdown', '~> 1.3.3' |
65 |
-gem 'faraday', '~> 0.9.0' |
|
66 |
-gem 'faraday_middleware' |
|
67 |
-gem 'typhoeus', '~> 0.6.3' |
|
54 |
+gem 'liquid', '~> 2.6.1' |
|
55 |
+gem 'mysql2', '~> 0.3.16' |
|
56 |
+gem 'multi_xml' |
|
68 | 57 |
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 | 58 |
gem 'omniauth' |
89 | 59 |
gem 'omniauth-twitter' |
90 |
-gem 'omniauth-37signals' |
|
91 |
-gem 'omniauth-github' |
|
60 |
+gem 'rails' , '4.1.5' |
|
61 |
+gem 'rufus-scheduler', '~> 3.0.8', require: false |
|
62 |
+gem 'sass-rails', '~> 4.0.0' |
|
63 |
+gem 'select2-rails', '~> 3.5.4' |
|
64 |
+gem 'spectrum-rails' |
|
65 |
+gem 'therubyracer', '~> 0.12.1' |
|
66 |
+gem 'twitter', '~> 5.8.0' |
|
67 |
+gem 'typhoeus', '~> 0.6.3' |
|
68 |
+gem 'uglifier', '>= 1.3.0' |
|
92 | 69 |
|
93 | 70 |
group :development do |
94 |
- gem 'binding_of_caller' |
|
95 | 71 |
gem 'better_errors', '~> 1.1' |
72 |
+ gem 'binding_of_caller' |
|
96 | 73 |
gem 'quiet_assets' |
97 | 74 |
end |
98 | 75 |
|
99 | 76 |
group :development, :test do |
100 |
- gem 'vcr' |
|
77 |
+ gem 'coveralls', require: false |
|
78 |
+ gem 'delorean' |
|
101 | 79 |
gem 'dotenv-rails' |
102 | 80 |
gem 'pry' |
103 |
- gem 'rspec-rails', '~> 2.99' |
|
81 |
+ gem 'rr' |
|
104 | 82 |
gem 'rspec', '~> 2.99' |
105 | 83 |
gem 'rspec-collection_matchers' |
84 |
+ gem 'rspec-rails', '~> 2.99' |
|
106 | 85 |
gem 'shoulda-matchers' |
107 |
- gem 'rr' |
|
108 |
- gem 'delorean' |
|
109 |
- gem 'webmock', '~> 1.17.4', require: false |
|
110 |
- gem 'coveralls', require: false |
|
111 | 86 |
gem 'spring' |
112 | 87 |
gem 'spring-commands-rspec' |
88 |
+ gem 'vcr' |
|
89 |
+ gem 'webmock', '~> 1.17.4', require: false |
|
113 | 90 |
end |
114 | 91 |
|
115 | 92 |
group :production do |
@@ -117,6 +94,23 @@ group :production do |
||
117 | 94 |
gem 'rack' |
118 | 95 |
end |
119 | 96 |
|
97 |
+case RUBY_PLATFORM |
|
98 |
+ when /freebsd|netbsd|openbsd/ |
|
99 |
+ # ffi (required by typhoeus via ethon) merged fixes for bugs fatal |
|
100 |
+ # on these platforms after 1.9.3; no following release as yet. |
|
101 |
+ gem 'ffi', github: 'ffi/ffi', branch: 'master' |
|
102 |
+ |
|
103 |
+ # tzinfo 1.2.0 has added support for reading zoneinfo on these |
|
104 |
+ # platforms. |
|
105 |
+ gem 'tzinfo', '>= 1.2.0' |
|
106 |
+ when /solaris/ |
|
107 |
+ # ditto |
|
108 |
+ gem 'tzinfo', '>= 1.2.0' |
|
109 |
+end |
|
110 |
+ |
|
111 |
+# Windows does not have zoneinfo files, so bundle the tzinfo-data gem. |
|
112 |
+gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw] |
|
113 |
+ |
|
120 | 114 |
# This hack needs some explanation. When on Heroku, use the pg, unicorn, and rails12factor gems. |
121 | 115 |
# When not on Heroku, we still want our Gemfile.lock to include these gems, so we scope them to |
122 | 116 |
# 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) |
@@ -423,12 +420,12 @@ DEPENDENCIES |
||
423 | 420 |
kramdown (~> 1.3.3) |
424 | 421 |
liquid (~> 2.6.1) |
425 | 422 |
mqtt |
423 |
+ multi_xml |
|
426 | 424 |
mysql2 (~> 0.3.16) |
427 | 425 |
net-ftp-list (~> 3.2.8) |
428 | 426 |
nokogiri (~> 1.6.1) |
429 | 427 |
omniauth |
430 | 428 |
omniauth-37signals |
431 |
- omniauth-github |
|
432 | 429 |
omniauth-twitter |
433 | 430 |
pg |
434 | 431 |
protected_attributes (~> 1.0.8) |
@@ -170,7 +170,7 @@ span.not-applicable:after { |
||
170 | 170 |
|
171 | 171 |
// Disabled |
172 | 172 |
|
173 |
-.agent-disabled { |
|
173 |
+.agent-unavailable { |
|
174 | 174 |
opacity: 0.5; |
175 | 175 |
} |
176 | 176 |
|
@@ -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 |