@@ -1,5 +1,33 @@ |
||
| 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 |
+# Twitter Agents |
|
| 19 |
+gem 'twitter', '~> 5.8.0' # Must to be loaded before cantino-twitter-stream. |
|
| 20 |
+gem 'cantino-twitter-stream', github: 'cantino/twitter-stream', branch: 'master' |
|
| 21 |
+gem 'omniauth-twitter' |
|
| 22 |
+ |
|
| 23 |
+# Tumblr Agents |
|
| 24 |
+gem 'tumblr_client' |
|
| 25 |
+gem 'omniauth-tumblr' |
|
| 26 |
+ |
|
| 27 |
+# Optional Services. |
|
| 28 |
+gem 'omniauth-37signals' # BasecampAgent |
|
| 29 |
+# gem 'omniauth-github' |
|
| 30 |
+ |
|
| 3 | 31 |
# Bundler <1.5 does not recognize :x64_mingw as a valid platform name. |
| 4 | 32 |
# Unfortunately, it can't self-update because it errors when encountering :x64_mingw. |
| 5 | 33 |
unless Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('1.5.0')
|
@@ -7,111 +35,66 @@ unless Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('1.5.0')
|
||
| 7 | 35 |
exit 1 |
| 8 | 36 |
end |
| 9 | 37 |
|
| 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' |
|
| 38 |
+gem 'protected_attributes', '~>1.0.8' # This must be loaded before some other gems, like delayed_job. |
|
| 21 | 39 |
|
| 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] |
|
| 32 |
- |
|
| 33 |
-gem 'mysql2', '~> 0.3.16' |
|
| 34 |
-gem 'devise', '~> 3.2.4' |
|
| 35 |
-gem 'kaminari', '~> 0.16.1' |
|
| 40 |
+gem 'ace-rails-ap', '~> 2.0.1' |
|
| 36 | 41 |
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 |
- |
|
| 42 |
+gem 'bundler', '>= 1.5.0' |
|
| 43 |
+gem 'coffee-rails', '~> 4.0.0' |
|
| 44 |
+gem 'daemons', '~> 1.1.9' |
|
| 44 | 45 |
gem 'delayed_job', '~> 4.0.0' |
| 45 | 46 |
gem 'delayed_job_active_record', '~> 4.0.0' |
| 46 |
-gem 'daemons', '~> 1.1.9' |
|
| 47 |
- |
|
| 47 |
+gem 'devise', '~> 3.2.4' |
|
| 48 |
+gem 'em-http-request', '~> 1.1.2' |
|
| 49 |
+gem 'faraday', '~> 0.9.0' |
|
| 50 |
+gem 'faraday_middleware' |
|
| 51 |
+gem 'feed-normalizer' |
|
| 52 |
+gem 'font-awesome-sass' |
|
| 48 | 53 |
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 | 54 |
# geokit-rails doesn't work with geokit 1.8.X but it specifies ~> 1.5 |
| 60 | 55 |
# in its own Gemfile. |
| 61 | 56 |
gem 'geokit', '~> 1.8.4' |
| 62 | 57 |
gem 'geokit-rails', '~> 2.0.1' |
| 63 |
- |
|
| 58 |
+gem 'httparty', '~> 0.13' |
|
| 59 |
+gem 'jquery-rails', '~> 3.1.0' |
|
| 60 |
+gem 'json', '~> 1.8.1' |
|
| 61 |
+gem 'jsonpath', '~> 0.5.6' |
|
| 62 |
+gem 'kaminari', '~> 0.16.1' |
|
| 64 | 63 |
gem 'kramdown', '~> 1.3.3' |
| 65 |
-gem 'faraday', '~> 0.9.0' |
|
| 66 |
-gem 'faraday_middleware' |
|
| 67 |
-gem 'typhoeus', '~> 0.6.3' |
|
| 64 |
+gem 'liquid', '~> 2.6.1' |
|
| 65 |
+gem 'mysql2', '~> 0.3.16' |
|
| 66 |
+gem 'multi_xml' |
|
| 68 | 67 |
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 |
-gem 'tumblr_client' |
|
| 88 |
- |
|
| 89 | 68 |
gem 'omniauth' |
| 90 |
-gem 'omniauth-twitter' |
|
| 91 |
-gem 'omniauth-37signals' |
|
| 92 |
-gem 'omniauth-github' |
|
| 93 |
-gem 'omniauth-tumblr' |
|
| 69 |
+gem 'rails' , '4.1.5' |
|
| 70 |
+gem 'rufus-scheduler', '~> 3.0.8', require: false |
|
| 71 |
+gem 'sass-rails', '~> 4.0.0' |
|
| 72 |
+gem 'select2-rails', '~> 3.5.4' |
|
| 73 |
+gem 'spectrum-rails' |
|
| 74 |
+gem 'therubyracer', '~> 0.12.1' |
|
| 75 |
+gem 'typhoeus', '~> 0.6.3' |
|
| 76 |
+gem 'uglifier', '>= 1.3.0' |
|
| 94 | 77 |
|
| 95 | 78 |
group :development do |
| 96 |
- gem 'binding_of_caller' |
|
| 97 | 79 |
gem 'better_errors', '~> 1.1' |
| 80 |
+ gem 'binding_of_caller' |
|
| 98 | 81 |
gem 'quiet_assets' |
| 99 | 82 |
end |
| 100 | 83 |
|
| 101 | 84 |
group :development, :test do |
| 102 |
- gem 'vcr' |
|
| 85 |
+ gem 'coveralls', require: false |
|
| 86 |
+ gem 'delorean' |
|
| 103 | 87 |
gem 'dotenv-rails' |
| 104 | 88 |
gem 'pry' |
| 105 |
- gem 'rspec-rails', '~> 2.99' |
|
| 89 |
+ gem 'rr' |
|
| 106 | 90 |
gem 'rspec', '~> 2.99' |
| 107 | 91 |
gem 'rspec-collection_matchers' |
| 92 |
+ gem 'rspec-rails', '~> 2.99' |
|
| 108 | 93 |
gem 'shoulda-matchers' |
| 109 |
- gem 'rr' |
|
| 110 |
- gem 'delorean' |
|
| 111 |
- gem 'webmock', '~> 1.17.4', require: false |
|
| 112 |
- gem 'coveralls', require: false |
|
| 113 | 94 |
gem 'spring' |
| 114 | 95 |
gem 'spring-commands-rspec' |
| 96 |
+ gem 'vcr' |
|
| 97 |
+ gem 'webmock', '~> 1.17.4', require: false |
|
| 115 | 98 |
end |
| 116 | 99 |
|
| 117 | 100 |
group :production do |
@@ -119,6 +102,12 @@ group :production do |
||
| 119 | 102 |
gem 'rack' |
| 120 | 103 |
end |
| 121 | 104 |
|
| 105 |
+# Platform requirements. |
|
| 106 |
+gem 'ffi', '>= 1.9.4' # required by typhoeus; 1.9.4 has fixes for *BSD. |
|
| 107 |
+gem 'tzinfo', '>= 1.2.0' # required by rails; 1.2.0 has support for *BSD and Solaris. |
|
| 108 |
+# Windows does not have zoneinfo files, so bundle the tzinfo-data gem. |
|
| 109 |
+gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw] |
|
| 110 |
+ |
|
| 122 | 111 |
# This hack needs some explanation. When on Heroku, use the pg, unicorn, and rails12factor gems. |
| 123 | 112 |
# When not on Heroku, we still want our Gemfile.lock to include these gems, so we scope them to |
| 124 | 113 |
# an unsupported platform. |
@@ -119,7 +119,9 @@ GEM |
||
| 119 | 119 |
feed-normalizer (1.5.2) |
| 120 | 120 |
hpricot (>= 0.6) |
| 121 | 121 |
simple-rss (>= 1.1) |
| 122 |
- ffi (1.9.3) |
|
| 122 |
+ ffi (1.9.5) |
|
| 123 |
+ font-awesome-sass (4.2.0) |
|
| 124 |
+ sass (~> 3.2) |
|
| 123 | 125 |
forecast_io (2.0.0) |
| 124 | 126 |
faraday |
| 125 | 127 |
hashie |
@@ -204,9 +206,6 @@ GEM |
||
| 204 | 206 |
omniauth-37signals (1.0.5) |
| 205 | 207 |
omniauth (~> 1.0) |
| 206 | 208 |
omniauth-oauth2 (~> 1.0) |
| 207 |
- omniauth-github (1.1.2) |
|
| 208 |
- omniauth (~> 1.0) |
|
| 209 |
- omniauth-oauth2 (~> 1.1) |
|
| 210 | 209 |
omniauth-oauth (1.0.1) |
| 211 | 210 |
oauth |
| 212 | 211 |
omniauth (~> 1.0) |
@@ -419,12 +418,15 @@ DEPENDENCIES |
||
| 419 | 418 |
faraday (~> 0.9.0) |
| 420 | 419 |
faraday_middleware |
| 421 | 420 |
feed-normalizer |
| 421 |
+ ffi (>= 1.9.4) |
|
| 422 |
+ font-awesome-sass |
|
| 422 | 423 |
forecast_io (~> 2.0.0) |
| 423 | 424 |
foreman (~> 0.63.0) |
| 424 | 425 |
geokit (~> 1.8.4) |
| 425 | 426 |
geokit-rails (~> 2.0.1) |
| 426 | 427 |
google-api-client |
| 427 | 428 |
hipchat (~> 1.2.0) |
| 429 |
+ httparty (~> 0.13) |
|
| 428 | 430 |
jquery-rails (~> 3.1.0) |
| 429 | 431 |
json (~> 1.8.1) |
| 430 | 432 |
jsonpath (~> 0.5.6) |
@@ -432,13 +434,12 @@ DEPENDENCIES |
||
| 432 | 434 |
kramdown (~> 1.3.3) |
| 433 | 435 |
liquid (~> 2.6.1) |
| 434 | 436 |
mqtt |
| 437 |
+ multi_xml |
|
| 435 | 438 |
mysql2 (~> 0.3.16) |
| 436 | 439 |
net-ftp-list (~> 3.2.8) |
| 437 | 440 |
nokogiri (~> 1.6.1) |
| 438 | 441 |
omniauth |
| 439 | 442 |
omniauth-37signals |
| 440 |
- omniauth-github |
|
| 441 |
- omniauth-tumblr |
|
| 442 | 443 |
omniauth-twitter |
| 443 | 444 |
pg |
| 444 | 445 |
protected_attributes (~> 1.0.8) |
@@ -466,6 +467,7 @@ DEPENDENCIES |
||
| 466 | 467 |
twilio-ruby (~> 3.11.5) |
| 467 | 468 |
twitter (~> 5.8.0) |
| 468 | 469 |
typhoeus (~> 0.6.3) |
| 470 |
+ tzinfo (>= 1.2.0) |
|
| 469 | 471 |
tzinfo-data |
| 470 | 472 |
uglifier (>= 1.3.0) |
| 471 | 473 |
unicorn |
@@ -55,6 +55,7 @@ If you just want to play around, you can simply fork this repository, then perfo |
||
| 55 | 55 |
|
| 56 | 56 |
* Run `git remote add upstream https://github.com/cantino/huginn.git` to add the main repository as a remote for your fork. |
| 57 | 57 |
* Copy `.env.example` to `.env` (`cp .env.example .env`) and edit `.env`, at least updating the `APP_SECRET_TOKEN` variable. |
| 58 |
+* Run `bundle` to install dependencies |
|
| 58 | 59 |
* Run `rake db:create`, `rake db:migrate`, and then `rake db:seed` to create a development MySQL database with some example Agents. |
| 59 | 60 |
* Run `foreman start`, visit [http://localhost:3000/][localhost], and login with the username of `admin` and the password of `password`. |
| 60 | 61 |
* Setup some Agents! |
@@ -0,0 +1,12 @@ |
||
| 1 |
+#= require jquery |
|
| 2 |
+#= require jquery_ujs |
|
| 3 |
+#= require typeahead.bundle |
|
| 4 |
+#= require bootstrap |
|
| 5 |
+#= require select2 |
|
| 6 |
+#= require json2 |
|
| 7 |
+#= require jquery.json-editor |
|
| 8 |
+#= require latlon_and_geo |
|
| 9 |
+#= require spectrum |
|
| 10 |
+#= require_tree ./components |
|
| 11 |
+#= require_tree ./pages |
|
| 12 |
+#= require_self |
@@ -1,226 +0,0 @@ |
||
| 1 |
-#= require jquery |
|
| 2 |
-#= require jquery_ujs |
|
| 3 |
-#= require typeahead.bundle |
|
| 4 |
-#= require bootstrap |
|
| 5 |
-#= require select2 |
|
| 6 |
-#= require json2 |
|
| 7 |
-#= require jquery.json-editor |
|
| 8 |
-#= require latlon_and_geo |
|
| 9 |
-#= require spectrum |
|
| 10 |
-#= require ./worker-checker |
|
| 11 |
-#= require_self |
|
| 12 |
- |
|
| 13 |
-window.setupJsonEditor = ($editors = $(".live-json-editor")) ->
|
|
| 14 |
- JSONEditor.prototype.ADD_IMG = '<%= image_path 'json-editor/add.png' %>' |
|
| 15 |
- JSONEditor.prototype.DELETE_IMG = '<%= image_path 'json-editor/delete.png' %>' |
|
| 16 |
- editors = [] |
|
| 17 |
- $editors.each -> |
|
| 18 |
- $editor = $(this) |
|
| 19 |
- jsonEditor = new JSONEditor($editor, $editor.data('width') || 400, $editor.data('height') || 500)
|
|
| 20 |
- jsonEditor.doTruncation true |
|
| 21 |
- jsonEditor.showFunctionButtons() |
|
| 22 |
- editors.push jsonEditor |
|
| 23 |
- return editors |
|
| 24 |
- |
|
| 25 |
-hideSchedule = -> |
|
| 26 |
- $(".schedule-region .can-be-scheduled").hide()
|
|
| 27 |
- $(".schedule-region .cannot-be-scheduled").show()
|
|
| 28 |
- |
|
| 29 |
-showSchedule = (defaultSchedule = null) -> |
|
| 30 |
- if defaultSchedule? |
|
| 31 |
- $(".schedule-region select").val(defaultSchedule).change()
|
|
| 32 |
- $(".schedule-region .can-be-scheduled").show()
|
|
| 33 |
- $(".schedule-region .cannot-be-scheduled").hide()
|
|
| 34 |
- |
|
| 35 |
-hideLinks = -> |
|
| 36 |
- $(".link-region .select2-container").hide()
|
|
| 37 |
- $(".link-region .propagate-immediately").hide()
|
|
| 38 |
- $(".link-region .cannot-receive-events").show()
|
|
| 39 |
- |
|
| 40 |
-showLinks = -> |
|
| 41 |
- $(".link-region .select2-container").show()
|
|
| 42 |
- $(".link-region .propagate-immediately").show()
|
|
| 43 |
- $(".link-region .cannot-receive-events").hide()
|
|
| 44 |
- showEventDescriptions() |
|
| 45 |
- |
|
| 46 |
-hideControlLinks = -> |
|
| 47 |
- $(".control-link-region").hide()
|
|
| 48 |
- |
|
| 49 |
-showControlLinks = -> |
|
| 50 |
- $(".control-link-region").show()
|
|
| 51 |
- |
|
| 52 |
-hideEventCreation = -> |
|
| 53 |
- $(".event-related-region").hide()
|
|
| 54 |
- |
|
| 55 |
-showEventCreation = -> |
|
| 56 |
- $(".event-related-region").show()
|
|
| 57 |
- |
|
| 58 |
-showEventDescriptions = -> |
|
| 59 |
- if $("#agent_source_ids").val()
|
|
| 60 |
- $.getJSON "/agents/event_descriptions", { ids: $("#agent_source_ids").val().join(",") }, (json) =>
|
|
| 61 |
- if json.description_html? |
|
| 62 |
- $(".event-descriptions").show().html(json.description_html)
|
|
| 63 |
- else |
|
| 64 |
- $(".event-descriptions").hide()
|
|
| 65 |
- else |
|
| 66 |
- $(".event-descriptions").html("").hide()
|
|
| 67 |
- |
|
| 68 |
-$(document).ready -> |
|
| 69 |
- $('.navbar .dropdown.dropdown-hover').hover \
|
|
| 70 |
- -> $(this).addClass('open'),
|
|
| 71 |
- -> $(this).removeClass('open')
|
|
| 72 |
- |
|
| 73 |
- # JSON Editor |
|
| 74 |
- window.jsonEditor = setupJsonEditor()[0] |
|
| 75 |
- |
|
| 76 |
- # Flash |
|
| 77 |
- if $(".flash").length
|
|
| 78 |
- setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000)
|
|
| 79 |
- |
|
| 80 |
- # Help popovers |
|
| 81 |
- $('.hover-help').popover(trigger: 'hover', html: true)
|
|
| 82 |
- |
|
| 83 |
- # Agent Navigation |
|
| 84 |
- $agentNavigate = $('#agent-navigate')
|
|
| 85 |
- |
|
| 86 |
- # initialize typeahead listener |
|
| 87 |
- $agentNavigate.bind "typeahead:selected", (event, object, name) -> |
|
| 88 |
- item = object['value'] |
|
| 89 |
- $agentNavigate.typeahead('val', '')
|
|
| 90 |
- if agentPaths[item] |
|
| 91 |
- $(".spinner").show()
|
|
| 92 |
- navigationData = agentPaths[item] |
|
| 93 |
- if !(navigationData instanceof Object) || !navigationData.method || navigationData.method == 'GET' |
|
| 94 |
- window.location = navigationData.url || navigationData |
|
| 95 |
- else |
|
| 96 |
- $("<a href='#{navigationData.url}' data-method='#{navigationData.method}'></a>").appendTo($("body")).click()
|
|
| 97 |
- |
|
| 98 |
- # substring matcher for typeahead |
|
| 99 |
- substringMatcher = (strings)-> |
|
| 100 |
- findMatches = (query, callback) -> |
|
| 101 |
- matches = [] |
|
| 102 |
- substrRegex = new RegExp(query, "i") |
|
| 103 |
- $.each strings, (i, str) -> |
|
| 104 |
- matches.push value: str if substrRegex.test(str) |
|
| 105 |
- callback(matches.slice(0,6)) |
|
| 106 |
- |
|
| 107 |
- $agentNavigate.typeahead |
|
| 108 |
- minLength: 1, |
|
| 109 |
- highlight: true, |
|
| 110 |
- , |
|
| 111 |
- source: substringMatcher(agentNames) |
|
| 112 |
- |
|
| 113 |
- |
|
| 114 |
- # Pressing '/' selects the search box. |
|
| 115 |
- $("body").on "keypress", (e) ->
|
|
| 116 |
- if e.keyCode == 47 # The '/' key |
|
| 117 |
- if e.target.nodeName == "BODY" |
|
| 118 |
- e.preventDefault() |
|
| 119 |
- $agentNavigate.focus() |
|
| 120 |
- |
|
| 121 |
- # Agent Show |
|
| 122 |
- fetchLogs = (e) -> |
|
| 123 |
- agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
|
|
| 124 |
- e.preventDefault() |
|
| 125 |
- $("#logs .spinner").show()
|
|
| 126 |
- $("#logs .refresh, #logs .clear").hide()
|
|
| 127 |
- $.get "/agents/#{agentId}/logs", (html) =>
|
|
| 128 |
- $("#logs .logs").html html
|
|
| 129 |
- $("#logs .spinner").stop(true, true).fadeOut ->
|
|
| 130 |
- $("#logs .refresh, #logs .clear").show()
|
|
| 131 |
- |
|
| 132 |
- clearLogs = (e) -> |
|
| 133 |
- if confirm("Are you sure you want to clear all logs for this Agent?")
|
|
| 134 |
- agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
|
|
| 135 |
- e.preventDefault() |
|
| 136 |
- $("#logs .spinner").show()
|
|
| 137 |
- $("#logs .refresh, #logs .clear").hide()
|
|
| 138 |
- $.post "/agents/#{agentId}/logs/clear", { "_method": "DELETE" }, (html) =>
|
|
| 139 |
- $("#logs .logs").html html
|
|
| 140 |
- $("#show-tabs li a.recent-errors").removeClass 'recent-errors'
|
|
| 141 |
- $("#logs .spinner").stop(true, true).fadeOut ->
|
|
| 142 |
- $("#logs .refresh, #logs .clear").show()
|
|
| 143 |
- |
|
| 144 |
- $(".agent-show #show-tabs a[href='#logs'], #logs .refresh").on "click", fetchLogs
|
|
| 145 |
- $(".agent-show #logs .clear").on "click", clearLogs
|
|
| 146 |
- |
|
| 147 |
- if tab = window.location.href.match(/tab=(\w+)\b/i)?[1] |
|
| 148 |
- if tab in ["details", "logs"] |
|
| 149 |
- $(".agent-show .nav-pills li a[href='##{tab}']").click()
|
|
| 150 |
- |
|
| 151 |
- # Editing Agents |
|
| 152 |
- $("#agent_source_ids").on "change", showEventDescriptions
|
|
| 153 |
- |
|
| 154 |
- $("#agent_type").on "change", ->
|
|
| 155 |
- if window.jsonEditor? |
|
| 156 |
- $("#agent-spinner").fadeIn();
|
|
| 157 |
- $("#agent_source_ids").select2("val", {});
|
|
| 158 |
- $(".event-descriptions").html("").hide()
|
|
| 159 |
- $.getJSON "/agents/type_details", { type: $(@).val() }, (json) =>
|
|
| 160 |
- if json.can_be_scheduled |
|
| 161 |
- showSchedule(json.default_schedule) |
|
| 162 |
- else |
|
| 163 |
- hideSchedule() |
|
| 164 |
- |
|
| 165 |
- if json.can_receive_events |
|
| 166 |
- showLinks() |
|
| 167 |
- else |
|
| 168 |
- hideLinks() |
|
| 169 |
- |
|
| 170 |
- if json.can_control_other_agents |
|
| 171 |
- showControlLinks() |
|
| 172 |
- else |
|
| 173 |
- hideControlLinks() |
|
| 174 |
- |
|
| 175 |
- if json.can_create_events |
|
| 176 |
- showEventCreation() |
|
| 177 |
- else |
|
| 178 |
- hideEventCreation() |
|
| 179 |
- |
|
| 180 |
- $(".description").html(json.description_html) if json.description_html?
|
|
| 181 |
- |
|
| 182 |
- $('.oauthable-form').html(json.form) if json.form?
|
|
| 183 |
- |
|
| 184 |
- if $("#agent_options").hasClass("showing-default") || $("#agent_options").val().match(/\A\s*(\{\s*\}|)\s*\Z/g)
|
|
| 185 |
- window.jsonEditor.json = json.options |
|
| 186 |
- window.jsonEditor.rebuild() |
|
| 187 |
- |
|
| 188 |
- $("#agent-spinner").stop(true, true).fadeOut();
|
|
| 189 |
- |
|
| 190 |
- $("#agent_type").change() if $("#agent_type").length
|
|
| 191 |
- |
|
| 192 |
- # Select2 Selects |
|
| 193 |
- $(".select2").select2(width: 'resolve')
|
|
| 194 |
- |
|
| 195 |
- if $(".schedule-region")
|
|
| 196 |
- if $(".schedule-region").data("can-be-scheduled") == true
|
|
| 197 |
- showSchedule() |
|
| 198 |
- else |
|
| 199 |
- hideSchedule() |
|
| 200 |
- |
|
| 201 |
- if $(".link-region")
|
|
| 202 |
- if $(".link-region").data("can-receive-events") == true
|
|
| 203 |
- showLinks() |
|
| 204 |
- else |
|
| 205 |
- hideLinks() |
|
| 206 |
- |
|
| 207 |
- if $(".control-link-region")
|
|
| 208 |
- if $(".control-link-region").data("can-control-other-agents") == true
|
|
| 209 |
- showControlLinks() |
|
| 210 |
- else |
|
| 211 |
- hideControlLinks() |
|
| 212 |
- |
|
| 213 |
- if $(".event-related-region")
|
|
| 214 |
- if $(".event-related-region").data("can-create-events") == true
|
|
| 215 |
- showEventCreation() |
|
| 216 |
- else |
|
| 217 |
- hideEventCreation() |
|
| 218 |
- |
|
| 219 |
- $('.selectable-text').each ->
|
|
| 220 |
- $(this).click -> |
|
| 221 |
- range = document.createRange() |
|
| 222 |
- range.setStartBefore(this.firstChild) |
|
| 223 |
- range.setEndAfter(this.lastChild) |
|
| 224 |
- sel = window.getSelection() |
|
| 225 |
- sel.removeAllRanges(); |
|
| 226 |
- sel.addRange(range) |
@@ -0,0 +1,30 @@ |
||
| 1 |
+$ -> |
|
| 2 |
+ # Flash |
|
| 3 |
+ if $(".flash").length
|
|
| 4 |
+ setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000)
|
|
| 5 |
+ |
|
| 6 |
+ # Help popovers |
|
| 7 |
+ $('.hover-help').popover(trigger: 'hover', html: true)
|
|
| 8 |
+ |
|
| 9 |
+ # Pressing '/' selects the search box. |
|
| 10 |
+ $("body").on "keypress", (e) ->
|
|
| 11 |
+ if e.keyCode == 47 # The '/' key |
|
| 12 |
+ if e.target.nodeName == "BODY" |
|
| 13 |
+ e.preventDefault() |
|
| 14 |
+ $agentNavigate.focus() |
|
| 15 |
+ |
|
| 16 |
+ # Select2 Selects |
|
| 17 |
+ $(".select2").select2(width: 'resolve')
|
|
| 18 |
+ |
|
| 19 |
+ # Helper for selecting text when clicked |
|
| 20 |
+ $('.selectable-text').each ->
|
|
| 21 |
+ $(this).click -> |
|
| 22 |
+ range = document.createRange() |
|
| 23 |
+ range.setStartBefore(this.firstChild) |
|
| 24 |
+ range.setEndAfter(this.lastChild) |
|
| 25 |
+ sel = window.getSelection() |
|
| 26 |
+ sel.removeAllRanges(); |
|
| 27 |
+ sel.addRange(range) |
|
| 28 |
+ |
|
| 29 |
+ # Agent navbar dropdown |
|
| 30 |
+ $('.navbar .dropdown.dropdown-hover').hover (-> $(this).addClass('open')), (-> $(this).removeClass('open'))
|
@@ -0,0 +1,14 @@ |
||
| 1 |
+window.setupJsonEditor = ($editors = $(".live-json-editor")) ->
|
|
| 2 |
+ JSONEditor.prototype.ADD_IMG = '<%= image_path 'json-editor/add.png' %>' |
|
| 3 |
+ JSONEditor.prototype.DELETE_IMG = '<%= image_path 'json-editor/delete.png' %>' |
|
| 4 |
+ editors = [] |
|
| 5 |
+ $editors.each -> |
|
| 6 |
+ $editor = $(this) |
|
| 7 |
+ jsonEditor = new JSONEditor($editor, $editor.data('width') || 400, $editor.data('height') || 500)
|
|
| 8 |
+ jsonEditor.doTruncation true |
|
| 9 |
+ jsonEditor.showFunctionButtons() |
|
| 10 |
+ editors.push jsonEditor |
|
| 11 |
+ return editors |
|
| 12 |
+ |
|
| 13 |
+$ -> |
|
| 14 |
+ window.jsonEditor = setupJsonEditor()[0] |
@@ -0,0 +1,29 @@ |
||
| 1 |
+$ -> |
|
| 2 |
+ $agentNavigate = $('#agent-navigate')
|
|
| 3 |
+ |
|
| 4 |
+ # initialize typeahead listener |
|
| 5 |
+ $agentNavigate.bind "typeahead:selected", (event, object, name) -> |
|
| 6 |
+ item = object['value'] |
|
| 7 |
+ $agentNavigate.typeahead('val', '')
|
|
| 8 |
+ if window.agentPaths[item] |
|
| 9 |
+ $(".spinner").show()
|
|
| 10 |
+ navigationData = window.agentPaths[item] |
|
| 11 |
+ if !(navigationData instanceof Object) || !navigationData.method || navigationData.method == 'GET' |
|
| 12 |
+ window.location = navigationData.url || navigationData |
|
| 13 |
+ else |
|
| 14 |
+ $("<a href='#{navigationData.url}' data-method='#{navigationData.method}'></a>").appendTo($("body")).click()
|
|
| 15 |
+ |
|
| 16 |
+ # substring matcher for typeahead |
|
| 17 |
+ substringMatcher = (strings) -> |
|
| 18 |
+ findMatches = (query, callback) -> |
|
| 19 |
+ matches = [] |
|
| 20 |
+ substrRegex = new RegExp(query, "i") |
|
| 21 |
+ $.each strings, (i, str) -> |
|
| 22 |
+ matches.push value: str if substrRegex.test(str) |
|
| 23 |
+ callback(matches.slice(0,6)) |
|
| 24 |
+ |
|
| 25 |
+ $agentNavigate.typeahead |
|
| 26 |
+ minLength: 1, |
|
| 27 |
+ highlight: true, |
|
| 28 |
+ , |
|
| 29 |
+ source: substringMatcher(window.agentNames) |
@@ -0,0 +1,14 @@ |
||
| 1 |
+class @Utils |
|
| 2 |
+ @navigatePath: (path) -> |
|
| 3 |
+ path = "/" + path unless path.match(/^\//) |
|
| 4 |
+ window.location.href = path |
|
| 5 |
+ |
|
| 6 |
+ @currentPath: -> |
|
| 7 |
+ window.location.href.replace(/https?:\/\/.*?\//g, '') |
|
| 8 |
+ |
|
| 9 |
+ @registerPage: (klass, options = {}) ->
|
|
| 10 |
+ if options.forPathsMatching? |
|
| 11 |
+ if Utils.currentPath().match(options.forPathsMatching) |
|
| 12 |
+ window.currentPage = new klass() |
|
| 13 |
+ else |
|
| 14 |
+ new klass() |
@@ -1,3 +1,5 @@ |
||
| 1 |
+# This is not included in the core application.js bundle. |
|
| 2 |
+ |
|
| 1 | 3 |
$ -> |
| 2 | 4 |
svg = document.querySelector('.agent-diagram svg.diagram')
|
| 3 | 5 |
overlay = document.querySelector('.agent-diagram .overlay')
|
@@ -2,6 +2,8 @@ |
||
| 2 | 2 |
#= require rickshaw |
| 3 | 3 |
#= require_self |
| 4 | 4 |
|
| 5 |
+# This is not included in the core application.js bundle. |
|
| 6 |
+ |
|
| 5 | 7 |
window.renderGraph = ($chart, data, peaks, name) -> |
| 6 | 8 |
graph = new Rickshaw.Graph |
| 7 | 9 |
element: $chart.find(".chart").get(0)
|
@@ -0,0 +1,41 @@ |
||
| 1 |
+window.map_marker = (map, options = {}) ->
|
|
| 2 |
+ pos = new google.maps.LatLng(options.lat, options.lng) |
|
| 3 |
+ |
|
| 4 |
+ if options.radius > 0 |
|
| 5 |
+ new google.maps.Circle |
|
| 6 |
+ map: map |
|
| 7 |
+ strokeColor: '#FF0000' |
|
| 8 |
+ strokeOpacity: 0.8 |
|
| 9 |
+ strokeWeight: 2 |
|
| 10 |
+ fillColor: '#FF0000' |
|
| 11 |
+ fillOpacity: 0.35 |
|
| 12 |
+ center: pos |
|
| 13 |
+ radius: options.radius |
|
| 14 |
+ else |
|
| 15 |
+ new google.maps.Marker |
|
| 16 |
+ map: map |
|
| 17 |
+ position: pos |
|
| 18 |
+ title: 'Recorded Location' |
|
| 19 |
+ |
|
| 20 |
+ if options.course |
|
| 21 |
+ p1 = new LatLon(pos.lat(), pos.lng()) |
|
| 22 |
+ speed = options.speed ? 1 |
|
| 23 |
+ p2 = p1.destinationPoint(options.course, Math.max(0.2, speed) * 0.1) |
|
| 24 |
+ |
|
| 25 |
+ lineCoordinates = [ |
|
| 26 |
+ pos |
|
| 27 |
+ new google.maps.LatLng(p2.lat(), p2.lon()) |
|
| 28 |
+ ] |
|
| 29 |
+ |
|
| 30 |
+ lineSymbol = |
|
| 31 |
+ path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW |
|
| 32 |
+ |
|
| 33 |
+ new google.maps.Polyline |
|
| 34 |
+ map: map |
|
| 35 |
+ path: lineCoordinates |
|
| 36 |
+ icons: [ |
|
| 37 |
+ {
|
|
| 38 |
+ icon: lineSymbol |
|
| 39 |
+ offset: '100%' |
|
| 40 |
+ } |
|
| 41 |
+ ] |
@@ -0,0 +1,126 @@ |
||
| 1 |
+class @AgentEditPage |
|
| 2 |
+ constructor: -> |
|
| 3 |
+ $("#agent_source_ids").on "change", @showEventDescriptions
|
|
| 4 |
+ @showCorrectRegionsOnStartup() |
|
| 5 |
+ |
|
| 6 |
+ # The type selector is only available on the new agent form. |
|
| 7 |
+ if $("#agent_type").length
|
|
| 8 |
+ $("#agent_type").on "change", => @handleTypeChange(false)
|
|
| 9 |
+ @handleTypeChange(true) |
|
| 10 |
+ |
|
| 11 |
+ handleTypeChange: (firstTime) -> |
|
| 12 |
+ $(".event-descriptions").html("").hide()
|
|
| 13 |
+ type = $('#agent_type').val()
|
|
| 14 |
+ |
|
| 15 |
+ if type == 'Agent' |
|
| 16 |
+ $(".agent-settings").hide()
|
|
| 17 |
+ $(".description").hide()
|
|
| 18 |
+ else |
|
| 19 |
+ $(".agent-settings").show()
|
|
| 20 |
+ $("#agent-spinner").fadeIn()
|
|
| 21 |
+ $("#agent_source_ids").select2("val", {})
|
|
| 22 |
+ $(".model-errors").hide() unless firstTime
|
|
| 23 |
+ $.getJSON "/agents/type_details", { type: type }, (json) =>
|
|
| 24 |
+ if json.can_be_scheduled |
|
| 25 |
+ if firstTime |
|
| 26 |
+ @showSchedule() |
|
| 27 |
+ else |
|
| 28 |
+ @showSchedule(json.default_schedule) |
|
| 29 |
+ else |
|
| 30 |
+ @hideSchedule() |
|
| 31 |
+ |
|
| 32 |
+ if json.can_receive_events |
|
| 33 |
+ @showLinks() |
|
| 34 |
+ else |
|
| 35 |
+ @hideLinks() |
|
| 36 |
+ |
|
| 37 |
+ if json.can_control_other_agents |
|
| 38 |
+ @showControlLinks() |
|
| 39 |
+ else |
|
| 40 |
+ @hideControlLinks() |
|
| 41 |
+ |
|
| 42 |
+ if json.can_create_events |
|
| 43 |
+ @showEventCreation() |
|
| 44 |
+ else |
|
| 45 |
+ @hideEventCreation() |
|
| 46 |
+ |
|
| 47 |
+ $(".description").show().html(json.description_html) if json.description_html?
|
|
| 48 |
+ |
|
| 49 |
+ $('.oauthable-form').html(json.form) if json.form?
|
|
| 50 |
+ |
|
| 51 |
+ unless firstTime |
|
| 52 |
+ window.jsonEditor.json = json.options |
|
| 53 |
+ window.jsonEditor.rebuild() |
|
| 54 |
+ |
|
| 55 |
+ $("#agent-spinner").stop(true, true).fadeOut();
|
|
| 56 |
+ |
|
| 57 |
+ hideSchedule: -> |
|
| 58 |
+ $(".schedule-region .can-be-scheduled").hide()
|
|
| 59 |
+ $(".schedule-region .cannot-be-scheduled").show()
|
|
| 60 |
+ |
|
| 61 |
+ showSchedule: (defaultSchedule = null) -> |
|
| 62 |
+ if defaultSchedule? |
|
| 63 |
+ $(".schedule-region select").val(defaultSchedule).change()
|
|
| 64 |
+ $(".schedule-region .can-be-scheduled").show()
|
|
| 65 |
+ $(".schedule-region .cannot-be-scheduled").hide()
|
|
| 66 |
+ |
|
| 67 |
+ hideLinks: -> |
|
| 68 |
+ $(".link-region .select2-container").hide()
|
|
| 69 |
+ $(".link-region .propagate-immediately").hide()
|
|
| 70 |
+ $(".link-region .cannot-receive-events").show()
|
|
| 71 |
+ |
|
| 72 |
+ showLinks: -> |
|
| 73 |
+ $(".link-region .select2-container").show()
|
|
| 74 |
+ $(".link-region .propagate-immediately").show()
|
|
| 75 |
+ $(".link-region .cannot-receive-events").hide()
|
|
| 76 |
+ @showEventDescriptions() |
|
| 77 |
+ |
|
| 78 |
+ hideControlLinks: -> |
|
| 79 |
+ $(".control-link-region").hide()
|
|
| 80 |
+ |
|
| 81 |
+ showControlLinks: -> |
|
| 82 |
+ $(".control-link-region").show()
|
|
| 83 |
+ |
|
| 84 |
+ hideEventCreation: -> |
|
| 85 |
+ $(".event-related-region").hide()
|
|
| 86 |
+ |
|
| 87 |
+ showEventCreation: -> |
|
| 88 |
+ $(".event-related-region").show()
|
|
| 89 |
+ |
|
| 90 |
+ showEventDescriptions: -> |
|
| 91 |
+ if $("#agent_source_ids").val()
|
|
| 92 |
+ $.getJSON "/agents/event_descriptions", { ids: $("#agent_source_ids").val().join(",") }, (json) =>
|
|
| 93 |
+ if json.description_html? |
|
| 94 |
+ $(".event-descriptions").show().html(json.description_html)
|
|
| 95 |
+ else |
|
| 96 |
+ $(".event-descriptions").hide()
|
|
| 97 |
+ else |
|
| 98 |
+ $(".event-descriptions").html("").hide()
|
|
| 99 |
+ |
|
| 100 |
+ showCorrectRegionsOnStartup: -> |
|
| 101 |
+ if $(".schedule-region")
|
|
| 102 |
+ if $(".schedule-region").data("can-be-scheduled") == true
|
|
| 103 |
+ @showSchedule() |
|
| 104 |
+ else |
|
| 105 |
+ @hideSchedule() |
|
| 106 |
+ |
|
| 107 |
+ if $(".link-region")
|
|
| 108 |
+ if $(".link-region").data("can-receive-events") == true
|
|
| 109 |
+ @showLinks() |
|
| 110 |
+ else |
|
| 111 |
+ @hideLinks() |
|
| 112 |
+ |
|
| 113 |
+ if $(".control-link-region")
|
|
| 114 |
+ if $(".control-link-region").data("can-control-other-agents") == true
|
|
| 115 |
+ @showControlLinks() |
|
| 116 |
+ else |
|
| 117 |
+ @hideControlLinks() |
|
| 118 |
+ |
|
| 119 |
+ if $(".event-related-region")
|
|
| 120 |
+ if $(".event-related-region").data("can-create-events") == true
|
|
| 121 |
+ @showEventCreation() |
|
| 122 |
+ else |
|
| 123 |
+ @hideEventCreation() |
|
| 124 |
+ |
|
| 125 |
+$ -> |
|
| 126 |
+ Utils.registerPage(AgentEditPage, forPathsMatching: /^agents/) |
@@ -0,0 +1,35 @@ |
||
| 1 |
+class @AgentShowPage |
|
| 2 |
+ constructor: -> |
|
| 3 |
+ $(".agent-show #show-tabs a[href='#logs'], #logs .refresh").on "click", @fetchLogs
|
|
| 4 |
+ $(".agent-show #logs .clear").on "click", @clearLogs
|
|
| 5 |
+ |
|
| 6 |
+ # Trigger tabs when navigated to. |
|
| 7 |
+ if tab = window.location.href.match(/tab=(\w+)\b/i)?[1] |
|
| 8 |
+ if tab in ["details", "logs"] |
|
| 9 |
+ $(".agent-show .nav-pills li a[href='##{tab}']").click()
|
|
| 10 |
+ |
|
| 11 |
+ fetchLogs: (e) -> |
|
| 12 |
+ agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
|
|
| 13 |
+ e.preventDefault() |
|
| 14 |
+ $("#logs .spinner").show()
|
|
| 15 |
+ $("#logs .refresh, #logs .clear").hide()
|
|
| 16 |
+ $.get "/agents/#{agentId}/logs", (html) =>
|
|
| 17 |
+ $("#logs .logs").html html
|
|
| 18 |
+ $("#logs .spinner").stop(true, true).fadeOut ->
|
|
| 19 |
+ $("#logs .refresh, #logs .clear").show()
|
|
| 20 |
+ |
|
| 21 |
+ clearLogs: (e) -> |
|
| 22 |
+ if confirm("Are you sure you want to clear all logs for this Agent?")
|
|
| 23 |
+ agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
|
|
| 24 |
+ e.preventDefault() |
|
| 25 |
+ $("#logs .spinner").show()
|
|
| 26 |
+ $("#logs .refresh, #logs .clear").hide()
|
|
| 27 |
+ $.post "/agents/#{agentId}/logs/clear", { "_method": "DELETE" }, (html) =>
|
|
| 28 |
+ $("#logs .logs").html html
|
|
| 29 |
+ $("#show-tabs li a.recent-errors").removeClass 'recent-errors'
|
|
| 30 |
+ $("#logs .spinner").stop(true, true).fadeOut ->
|
|
| 31 |
+ $("#logs .refresh, #logs .clear").show()
|
|
| 32 |
+ |
|
| 33 |
+$ -> |
|
| 34 |
+ Utils.registerPage(AgentShowPage, forPathsMatching: /^agents\/\d+/) |
|
| 35 |
+ |
@@ -3,6 +3,8 @@ |
||
| 3 | 3 |
#= require ace/mode-markdown.js |
| 4 | 4 |
#= require_self |
| 5 | 5 |
|
| 6 |
+# This is not included in the core application.js bundle. |
|
| 7 |
+ |
|
| 6 | 8 |
$ -> |
| 7 | 9 |
editor = ace.edit("ace-credential-value")
|
| 8 | 10 |
editor.getSession().setTabSize(2) |
@@ -18,6 +18,8 @@ |
||
| 18 | 18 |
*/ |
| 19 | 19 |
|
| 20 | 20 |
@import "bootstrap"; |
| 21 |
+@import "font-awesome-sprockets"; |
|
| 22 |
+@import "font-awesome"; |
|
| 21 | 23 |
|
| 22 | 24 |
body { padding-top: 60px; }
|
| 23 | 25 |
|
@@ -86,6 +88,11 @@ span.not-applicable:after {
|
||
| 86 | 88 |
.nav > li {
|
| 87 | 89 |
&.job-indicator, &#event-indicator {
|
| 88 | 90 |
display: none; |
| 91 |
+ |
|
| 92 |
+ a {
|
|
| 93 |
+ padding-right: 5px; |
|
| 94 |
+ padding-left: 5px; |
|
| 95 |
+ } |
|
| 89 | 96 |
} |
| 90 | 97 |
} |
| 91 | 98 |
|
@@ -170,7 +177,7 @@ span.not-applicable:after {
|
||
| 170 | 177 |
|
| 171 | 178 |
// Disabled |
| 172 | 179 |
|
| 173 |
-.agent-disabled {
|
|
| 180 |
+.agent-unavailable {
|
|
| 174 | 181 |
opacity: 0.5; |
| 175 | 182 |
} |
| 176 | 183 |
|
@@ -232,3 +239,38 @@ h2 .scenario, a span.label.scenario {
|
||
| 232 | 239 |
.confirm-agent .popover {
|
| 233 | 240 |
width: 200px; |
| 234 | 241 |
} |
| 242 |
+ |
|
| 243 |
+.btn-auth {
|
|
| 244 |
+ position: relative; |
|
| 245 |
+ padding-left: 40px; |
|
| 246 |
+ $border-color: rgba(0,0,0,0.2); |
|
| 247 |
+ border-color: $border-color; |
|
| 248 |
+ |
|
| 249 |
+ > i:first-child {
|
|
| 250 |
+ position: absolute; |
|
| 251 |
+ top: 0; |
|
| 252 |
+ left: 0; |
|
| 253 |
+ bottom: 0; |
|
| 254 |
+ width: 32px; |
|
| 255 |
+ height: 32px; |
|
| 256 |
+ text-align: center; |
|
| 257 |
+ line-height: 32px; |
|
| 258 |
+ font-size: 24px; |
|
| 259 |
+ border-right: 1px solid $border-color; |
|
| 260 |
+ } |
|
| 261 |
+ |
|
| 262 |
+ &.btn-auth-twitter {
|
|
| 263 |
+ color: #fff; |
|
| 264 |
+ background-color: #55acee; |
|
| 265 |
+ } |
|
| 266 |
+ |
|
| 267 |
+ &.btn-auth-37signals {
|
|
| 268 |
+ color: #fff; |
|
| 269 |
+ background-color: #8fc857; |
|
| 270 |
+ } |
|
| 271 |
+ |
|
| 272 |
+ &.btn-auth-github {
|
|
| 273 |
+ color: #fff; |
|
| 274 |
+ background-color: #444; |
|
| 275 |
+ } |
|
| 276 |
+} |
@@ -6,17 +6,17 @@ |
||
| 6 | 6 |
&.asc:after, &.desc:after {
|
| 7 | 7 |
text-decoration: none; |
| 8 | 8 |
position: absolute; |
| 9 |
- top: -5px; |
|
| 10 |
- right: -12px; |
|
| 11 |
- font-size: 1.2em; |
|
| 9 |
+ top: 0; |
|
| 10 |
+ right: -1em; |
|
| 11 |
+ font-family: FontAwesome; |
|
| 12 | 12 |
} |
| 13 | 13 |
|
| 14 | 14 |
&.asc:after {
|
| 15 |
- content: '\2193'; |
|
| 15 |
+ content: '\f0de'; //fa-sort-asc |
|
| 16 | 16 |
} |
| 17 | 17 |
|
| 18 | 18 |
&.desc:after {
|
| 19 |
- content: '\2191'; |
|
| 19 |
+ content: '\f0dd'; //fa-sort-desc |
|
| 20 | 20 |
} |
| 21 | 21 |
} |
| 22 | 22 |
|
@@ -5,7 +5,9 @@ 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 |
+ |
|
| 10 |
+ gem_dependency_check { defined?(Twitter) && has_oauth_configuration_for?('twitter') }
|
|
| 9 | 11 |
end |
| 10 | 12 |
|
| 11 | 13 |
def validate_twitter_options |
@@ -41,4 +43,10 @@ module TwitterConcern |
||
| 41 | 43 |
config.access_token_secret = twitter_oauth_token_secret |
| 42 | 44 |
end |
| 43 | 45 |
end |
| 46 |
+ |
|
| 47 |
+ module ClassMethods |
|
| 48 |
+ def twitter_dependencies_missing |
|
| 49 |
+ "## Include the `twitter`, `omniauth-twitter`, and `cantino-twitter-stream` gems in your Gemfile to use Twitter Agents." |
|
| 50 |
+ end |
|
| 51 |
+ end |
|
| 44 | 52 |
end |
@@ -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 |
@@ -43,7 +43,7 @@ class AgentsController < ApplicationController |
||
| 43 | 43 |
:can_control_other_agents => @agent.can_control_other_agents?, |
| 44 | 44 |
:options => @agent.default_options, |
| 45 | 45 |
:description_html => @agent.html_description, |
| 46 |
- :form => render_to_string(partial: 'oauth_dropdown') |
|
| 46 |
+ :form => render_to_string(partial: 'oauth_dropdown', locals: { agent: @agent })
|
|
| 47 | 47 |
} |
| 48 | 48 |
end |
| 49 | 49 |
|
@@ -8,7 +8,9 @@ class UserCredentialsController < ApplicationController |
||
| 8 | 8 |
|
| 9 | 9 |
respond_to do |format| |
| 10 | 10 |
format.html |
| 11 |
- format.json { render json: @user_credentials }
|
|
| 11 |
+ format.json {
|
|
| 12 |
+ send_data Utils.pretty_jsonify(@user_credentials.limit(nil).as_json), disposition: 'attachment' |
|
| 13 |
+ } |
|
| 12 | 14 |
end |
| 13 | 15 |
end |
| 14 | 16 |
|
@@ -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,6 +1,5 @@ |
||
| 1 | 1 |
module Agents |
| 2 | 2 |
class AdiosoAgent < Agent |
| 3 |
- |
|
| 4 | 3 |
cannot_receive_events! |
| 5 | 4 |
|
| 6 | 5 |
default_schedule "every_1d" |
@@ -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 |
@@ -1,5 +1,3 @@ |
||
| 1 |
-require "twitter" |
|
| 2 |
- |
|
| 3 | 1 |
module Agents |
| 4 | 2 |
class TwitterPublishAgent < Agent |
| 5 | 3 |
include TwitterConcern |
@@ -7,6 +5,7 @@ module Agents |
||
| 7 | 5 |
cannot_be_scheduled! |
| 8 | 6 |
|
| 9 | 7 |
description <<-MD |
| 8 |
+ #{twitter_dependencies_missing if dependencies_missing?}
|
|
| 10 | 9 |
The TwitterPublishAgent publishes tweets from the events it receives. |
| 11 | 10 |
|
| 12 | 11 |
To be able to use this Agent you need to authenticate with Twitter in the [Services](/services) section first. |
@@ -5,6 +5,7 @@ module Agents |
||
| 5 | 5 |
cannot_receive_events! |
| 6 | 6 |
|
| 7 | 7 |
description <<-MD |
| 8 |
+ #{twitter_dependencies_missing if dependencies_missing?}
|
|
| 8 | 9 |
The TwitterStreamAgent follows the Twitter stream in real time, watching for certain keywords, or filters, that you provide. |
| 9 | 10 |
|
| 10 | 11 |
To follow the Twitter stream, provide an array of `filters`. Multiple words in a filter must all show up in a tweet, but are independent of order. |
@@ -1,5 +1,3 @@ |
||
| 1 |
-require "twitter" |
|
| 2 |
- |
|
| 3 | 1 |
module Agents |
| 4 | 2 |
class TwitterUserAgent < Agent |
| 5 | 3 |
include TwitterConcern |
@@ -7,6 +5,7 @@ module Agents |
||
| 7 | 5 |
cannot_receive_events! |
| 8 | 6 |
|
| 9 | 7 |
description <<-MD |
| 8 |
+ #{twitter_dependencies_missing if dependencies_missing?}
|
|
| 10 | 9 |
The TwitterUserAgent follows the timeline of a specified Twitter user. |
| 11 | 10 |
|
| 12 | 11 |
To be able to use this Agent you need to authenticate with Twitter in the [Services](/services) section first. |
@@ -65,8 +65,10 @@ module Agents |
||
| 65 | 65 |
private |
| 66 | 66 |
|
| 67 | 67 |
def handle_payload(payload) |
| 68 |
- if payload[:latitude].present? && payload[:longitude].present? |
|
| 69 |
- create_event payload: payload, lat: payload[:latitude].to_f, lng: payload[:longitude].to_f |
|
| 68 |
+ location = Location.new(payload) |
|
| 69 |
+ |
|
| 70 |
+ if location.present? |
|
| 71 |
+ create_event payload: payload, location: location |
|
| 70 | 72 |
end |
| 71 | 73 |
end |
| 72 | 74 |
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. |
@@ -14,7 +17,7 @@ module Agents |
||
| 14 | 17 |
|
| 15 | 18 |
The `location` can be a US zipcode, or any location that Wunderground supports. To find one, search [wunderground.com](http://wunderground.com) and copy the location part of the URL. For example, a result for San Francisco gives `http://www.wunderground.com/US/CA/San_Francisco.html` and London, England gives `http://www.wunderground.com/q/zmw:00000.1.03772`. The locations in each are `US/CA/San_Francisco` and `zmw:00000.1.03772`, respectively. |
| 16 | 19 |
|
| 17 |
- If you plan on using ForecastIO, the `location` must be a set of GPS coordinates. |
|
| 20 |
+ If you plan on using ForecastIO, the `location` must be a comma-separated string of co-ordinates (longitude, latitude). For example, San Francisco would be `37.7771,-122.4196`. |
|
| 18 | 21 |
|
| 19 | 22 |
You must setup an [API key for Wunderground](http://www.wunderground.com/weather/api/) in order to use this Agent with Wunderground. |
| 20 | 23 |
|
@@ -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. |
@@ -1,3 +1,5 @@ |
||
| 1 |
+require 'location' |
|
| 2 |
+ |
|
| 1 | 3 |
# Events are how Huginn Agents communicate and log information about the world. Events can be emitted and received by |
| 2 | 4 |
# Agents. They contain a serialized `payload` of arbitrary JSON data, as well as optional `lat`, `lng`, and `expires_at` |
| 3 | 5 |
# fields. |
@@ -5,7 +7,7 @@ class Event < ActiveRecord::Base |
||
| 5 | 7 |
include JSONSerializedField |
| 6 | 8 |
include LiquidDroppable |
| 7 | 9 |
|
| 8 |
- attr_accessible :lat, :lng, :payload, :user_id, :user, :expires_at |
|
| 10 |
+ attr_accessible :lat, :lng, :location, :payload, :user_id, :user, :expires_at |
|
| 9 | 11 |
|
| 10 | 12 |
acts_as_mappable |
| 11 | 13 |
|
@@ -28,6 +30,42 @@ class Event < ActiveRecord::Base |
||
| 28 | 30 |
where("expires_at IS NOT NULL AND expires_at < ?", Time.now)
|
| 29 | 31 |
} |
| 30 | 32 |
|
| 33 |
+ scope :with_location, -> {
|
|
| 34 |
+ where.not(lat: nil).where.not(lng: nil) |
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ def location |
|
| 38 |
+ @location ||= Location.new( |
|
| 39 |
+ # lat and lng are BigDecimal, but converted to Float by the Location class |
|
| 40 |
+ lat: lat, |
|
| 41 |
+ lng: lng, |
|
| 42 |
+ radius: |
|
| 43 |
+ begin |
|
| 44 |
+ h = payload[:horizontal_accuracy].presence |
|
| 45 |
+ v = payload[:vertical_accuracy].presence |
|
| 46 |
+ if h && v |
|
| 47 |
+ (h.to_f + v.to_f) / 2 |
|
| 48 |
+ else |
|
| 49 |
+ (h || v || payload[:accuracy]).to_f |
|
| 50 |
+ end |
|
| 51 |
+ end, |
|
| 52 |
+ course: payload[:course], |
|
| 53 |
+ speed: payload[:speed].presence) |
|
| 54 |
+ end |
|
| 55 |
+ |
|
| 56 |
+ def location=(location) |
|
| 57 |
+ case location |
|
| 58 |
+ when nil |
|
| 59 |
+ self.lat = self.lng = nil |
|
| 60 |
+ return |
|
| 61 |
+ when Location |
|
| 62 |
+ else |
|
| 63 |
+ location = Location.new(location) |
|
| 64 |
+ end |
|
| 65 |
+ self.lat, self.lng = location.lat, location.lng |
|
| 66 |
+ location |
|
| 67 |
+ end |
|
| 68 |
+ |
|
| 31 | 69 |
# Emit this event again, as a new Event. |
| 32 | 70 |
def reemit! |
| 33 | 71 |
agent.create_event :payload => payload, :lat => lat, :lng => lng |
@@ -79,4 +117,8 @@ class EventDrop |
||
| 79 | 117 |
@object.created_at |
| 80 | 118 |
} |
| 81 | 119 |
end |
| 120 |
+ |
|
| 121 |
+ def _location_ |
|
| 122 |
+ @object.location |
|
| 123 |
+ end |
|
| 82 | 124 |
end |
@@ -1,5 +1,5 @@ |
||
| 1 | 1 |
<% if @agent.errors.any? %> |
| 2 |
- <div class="row well"> |
|
| 2 |
+ <div class="row well model-errors"> |
|
| 3 | 3 |
<h2><%= pluralize(@agent.errors.count, "error") %> prohibited this Agent from being saved:</h2> |
| 4 | 4 |
<% @agent.errors.full_messages.each do |msg| %> |
| 5 | 5 |
<p class='text-warning'><%= msg %></p> |
@@ -21,99 +21,103 @@ |
||
| 21 | 21 |
<% if @agent.new_record? %> |
| 22 | 22 |
<div class="form-group type-select"> |
| 23 | 23 |
<%= f.label :type %> |
| 24 |
- <%= f.select :type, options_for_select(Agent.types.map(&:to_s).sort.map {|type| [type.gsub(/^.*::/, ''), type] }, @agent.type), {}, :class => 'select2 form-control' %>
|
|
| 24 |
+ <%= f.select :type, options_for_select([['Select an Agent Type', 'Agent']] + Agent.types.map(&:to_s).sort.map {|type| [type.gsub(/^.*::/, ''), type] }, @agent.type), {}, :class => 'select2 form-control' %>
|
|
| 25 | 25 |
</div> |
| 26 | 26 |
<% end %> |
| 27 |
+ </div> |
|
| 27 | 28 |
|
| 28 |
- <div class="form-group type-select"> |
|
| 29 |
- <%= f.label :name %> |
|
| 30 |
- <%= f.text_field :name, :class => 'form-control' %> |
|
| 31 |
- </div> |
|
| 32 |
- |
|
| 33 |
- <div class='oauthable-form'> |
|
| 34 |
- <%= render partial: 'oauth_dropdown' %> |
|
| 35 |
- </div> |
|
| 29 |
+ <div class="agent-settings"> |
|
| 30 |
+ <div class="col-md-8"> |
|
| 31 |
+ <div class="form-group"> |
|
| 32 |
+ <%= f.label :name %> |
|
| 33 |
+ <%= f.text_field :name, :class => 'form-control' %> |
|
| 34 |
+ </div> |
|
| 36 | 35 |
|
| 37 |
- <div class="form-group"> |
|
| 38 |
- <%= f.label :schedule, :class => 'control-label' %> |
|
| 39 |
- <div class="schedule-region" data-can-be-scheduled="<%= @agent.can_be_scheduled? %>"> |
|
| 40 |
- <div class="can-be-scheduled"> |
|
| 41 |
- <%= f.select :schedule, options_for_select(Agent::SCHEDULES.map {|s| [s.humanize.titleize, s] }, @agent.schedule), {}, :class => 'form-control' %>
|
|
| 42 |
- </div> |
|
| 43 |
- <span class='cannot-be-scheduled text-info'>This type of Agent cannot be scheduled.</span> |
|
| 36 |
+ <div class='oauthable-form'> |
|
| 37 |
+ <%= render partial: 'oauth_dropdown', locals: { agent: @agent } %>
|
|
| 44 | 38 |
</div> |
| 45 |
- </div> |
|
| 46 | 39 |
|
| 47 |
- <div class="controller-region" data-has-controllers="<%= !@agent.controllers.empty? %>"> |
|
| 48 | 40 |
<div class="form-group"> |
| 49 |
- <%= f.label :controllers %> |
|
| 50 |
- <span class="glyphicon glyphicon-question-sign hover-help" data-content="Other than the system-defined schedule above, this agent may be run or controlled by these user-defined Agents."></span> |
|
| 51 |
- <div class="controller-list"> |
|
| 52 |
- <%= agent_controllers(@agent) || 'None' %> |
|
| 41 |
+ <%= f.label :schedule, :class => 'control-label' %> |
|
| 42 |
+ <div class="schedule-region" data-can-be-scheduled="<%= @agent.can_be_scheduled? %>"> |
|
| 43 |
+ <div class="can-be-scheduled"> |
|
| 44 |
+ <%= f.select :schedule, options_for_select(Agent::SCHEDULES.map {|s| [s.humanize.titleize, s] }, @agent.schedule), {}, :class => 'form-control' %>
|
|
| 45 |
+ </div> |
|
| 46 |
+ <span class='cannot-be-scheduled text-info'>This type of Agent cannot be scheduled.</span> |
|
| 53 | 47 |
</div> |
| 54 | 48 |
</div> |
| 55 |
- </div> |
|
| 56 | 49 |
|
| 57 |
- <div class="control-link-region" data-can-control-other-agents="<%= @agent.can_control_other_agents? %>"> |
|
| 58 |
- <div class="can-control-other-agents"> |
|
| 50 |
+ <div class="controller-region" data-has-controllers="<%= !@agent.controllers.empty? %>"> |
|
| 59 | 51 |
<div class="form-group"> |
| 60 |
- <%= f.label :control_targets %> |
|
| 61 |
- <% eventControlTargets = current_user.agents.select(&:can_be_scheduled?) %> |
|
| 62 |
- <%= f.select(:control_target_ids, |
|
| 63 |
- options_for_select(eventControlTargets.map {|s| [s.name, s.id] },
|
|
| 64 |
- @agent.control_target_ids), |
|
| 65 |
- {}, { multiple: true, size: 5, class: 'select2 form-control' }) %>
|
|
| 52 |
+ <%= f.label :controllers %> |
|
| 53 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="Other than the system-defined schedule above, this agent may be run or controlled by these user-defined Agents."></span> |
|
| 54 |
+ <div class="controller-list"> |
|
| 55 |
+ <%= agent_controllers(@agent) || 'None' %> |
|
| 56 |
+ </div> |
|
| 66 | 57 |
</div> |
| 67 | 58 |
</div> |
| 68 |
- </div> |
|
| 69 | 59 |
|
| 70 |
- <div class='event-related-region' data-can-create-events="<%= @agent.can_create_events? %>"> |
|
| 71 |
- <div class="form-group"> |
|
| 72 |
- <%= f.label :keep_events_for, "Keep events" %> |
|
| 73 |
- <span class="glyphicon glyphicon-question-sign hover-help" data-content="In order to conserve disk space, you can choose to have events created by this Agent expire after a certain period of time. Make sure you keep them long enough to allow any subsequent Agents to make use of them."></span> |
|
| 74 |
- <%= f.select :keep_events_for, options_for_select(Agent::EVENT_RETENTION_SCHEDULES, @agent.keep_events_for), {}, :class => 'form-control' %>
|
|
| 60 |
+ <div class="control-link-region" data-can-control-other-agents="<%= @agent.can_control_other_agents? %>"> |
|
| 61 |
+ <div class="can-control-other-agents"> |
|
| 62 |
+ <div class="form-group"> |
|
| 63 |
+ <%= f.label :control_targets %> |
|
| 64 |
+ <% eventControlTargets = current_user.agents.select(&:can_be_scheduled?) %> |
|
| 65 |
+ <%= f.select(:control_target_ids, |
|
| 66 |
+ options_for_select(eventControlTargets.map {|s| [s.name, s.id] },
|
|
| 67 |
+ @agent.control_target_ids), |
|
| 68 |
+ {}, { multiple: true, size: 5, class: 'select2 form-control' }) %>
|
|
| 69 |
+ </div> |
|
| 70 |
+ </div> |
|
| 75 | 71 |
</div> |
| 76 |
- </div> |
|
| 77 | 72 |
|
| 78 |
- <div class="form-group"> |
|
| 79 |
- <%= f.label :sources %> |
|
| 80 |
- <div class="link-region" data-can-receive-events="<%= @agent.can_receive_events? %>"> |
|
| 81 |
- <% eventSources = (current_user.agents - [@agent]).find_all { |a| a.can_create_events? } %>
|
|
| 82 |
- <%= f.select(:source_ids, |
|
| 83 |
- options_for_select(eventSources.map {|s| [s.name, s.id] },
|
|
| 84 |
- @agent.source_ids), |
|
| 85 |
- {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %>
|
|
| 86 |
- <span class='cannot-receive-events text-info'>This type of Agent cannot receive events.</span> |
|
| 87 |
- <%= f.label :propagate_immediately, :class => 'propagate-immediately' do %>Propagate immediately |
|
| 88 |
- <%= f.check_box :propagate_immediately %> |
|
| 89 |
- <% end %> |
|
| 73 |
+ <div class='event-related-region' data-can-create-events="<%= @agent.can_create_events? %>"> |
|
| 74 |
+ <div class="form-group"> |
|
| 75 |
+ <%= f.label :keep_events_for, "Keep events" %> |
|
| 76 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="In order to conserve disk space, you can choose to have events created by this Agent expire after a certain period of time. Make sure you keep them long enough to allow any subsequent Agents to make use of them."></span> |
|
| 77 |
+ <%= f.select :keep_events_for, options_for_select(Agent::EVENT_RETENTION_SCHEDULES, @agent.keep_events_for), {}, :class => 'form-control' %>
|
|
| 78 |
+ </div> |
|
| 90 | 79 |
</div> |
| 91 |
- </div> |
|
| 92 | 80 |
|
| 93 |
- <% if current_user.scenario_count > 0 %> |
|
| 94 | 81 |
<div class="form-group"> |
| 95 |
- <%= f.label :scenarios %> |
|
| 96 |
- <span class="glyphicon glyphicon-question-sign hover-help" data-content="Use Scenarios to group sets of Agents, both for organization, and to make them easy to export and share."></span> |
|
| 97 |
- <%= f.select(:scenario_ids, |
|
| 98 |
- options_for_select(current_user.scenarios.pluck(:name, :id), @agent.scenario_ids), |
|
| 99 |
- {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %>
|
|
| 82 |
+ <%= f.label :sources %> |
|
| 83 |
+ <div class="link-region" data-can-receive-events="<%= @agent.can_receive_events? %>"> |
|
| 84 |
+ <% eventSources = (current_user.agents - [@agent]).find_all { |a| a.can_create_events? } %>
|
|
| 85 |
+ <%= f.select(:source_ids, |
|
| 86 |
+ options_for_select(eventSources.map {|s| [s.name, s.id] },
|
|
| 87 |
+ @agent.source_ids), |
|
| 88 |
+ {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %>
|
|
| 89 |
+ <span class='cannot-receive-events text-info'>This type of Agent cannot receive events.</span> |
|
| 90 |
+ <%= f.label :propagate_immediately, :class => 'propagate-immediately' do %>Propagate immediately |
|
| 91 |
+ <%= f.check_box :propagate_immediately %> |
|
| 92 |
+ <% end %> |
|
| 93 |
+ </div> |
|
| 100 | 94 |
</div> |
| 101 |
- <% end %> |
|
| 102 | 95 |
|
| 103 |
- </div> |
|
| 96 |
+ <% if current_user.scenario_count > 0 %> |
|
| 97 |
+ <div class="form-group"> |
|
| 98 |
+ <%= f.label :scenarios %> |
|
| 99 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="Use Scenarios to group sets of Agents, both for organization, and to make them easy to export and share."></span> |
|
| 100 |
+ <%= f.select(:scenario_ids, |
|
| 101 |
+ options_for_select(current_user.scenarios.pluck(:name, :id), @agent.scenario_ids), |
|
| 102 |
+ {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %>
|
|
| 103 |
+ </div> |
|
| 104 |
+ <% end %> |
|
| 104 | 105 |
|
| 105 |
- <!-- Form controls full width --> |
|
| 106 |
- <div class="col-md-12"> |
|
| 107 |
- <div class="form-group"> |
|
| 108 |
- <%= f.label :options %> |
|
| 109 |
- <span class="glyphicon glyphicon-question-sign hover-help" data-content="In this JSON hash, interpolation is available in almost all values using the Liquid templating language.<p>Available template variables include the following:<dl><dt><code>message</code>, <code>url</code>, etc.</dt><dd>Refers to the corresponding key's value of each incoming event's payload.</dd><dt><code>agent</code></dt><dd>Refers to the agent that created each incoming event. It has attributes like <code>type</code>, <code>name</code> and <code>options</code>, so <code>{{agent.type}}</code> will expand to <code>WebsiteAgent</code> if an incoming event is created by that agent.</dd></dl></p><p>To access user credentials, use the <code>credential</code> tag like this: <code>{% credential <em>bare_key_name</em> %}</code></p>"></span>
|
|
| 110 |
- <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor <%= (@agent.new_record? && @agent.options == {}) ? "showing-default" : "" %>">
|
|
| 111 |
- <%= Utils.jsonify((@agent.new_record? && @agent.options == {}) ? @agent.default_options : @agent.options) %>
|
|
| 112 |
- </textarea> |
|
| 113 | 106 |
</div> |
| 114 | 107 |
|
| 115 |
- <div class="form-group"> |
|
| 116 |
- <%= f.submit "Save", :class => "btn btn-primary" %> |
|
| 108 |
+ <!-- Form controls full width --> |
|
| 109 |
+ <div class="col-md-12"> |
|
| 110 |
+ <div class="form-group"> |
|
| 111 |
+ <%= f.label :options %> |
|
| 112 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="In this JSON hash, interpolation is available in almost all values using the Liquid templating language.<p>Available template variables include the following:<dl><dt><code>message</code>, <code>url</code>, etc.</dt><dd>Refers to the corresponding key's value of each incoming event's payload.</dd><dt><code>agent</code></dt><dd>Refers to the agent that created each incoming event. It has attributes like <code>type</code>, <code>name</code> and <code>options</code>, so <code>{{agent.type}}</code> will expand to <code>WebsiteAgent</code> if an incoming event is created by that agent.</dd></dl></p><p>To access user credentials, use the <code>credential</code> tag like this: <code>{% credential <em>bare_key_name</em> %}</code></p>"></span>
|
|
| 113 |
+ <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor"> |
|
| 114 |
+ <%= Utils.jsonify((@agent.new_record? && @agent.options == {}) ? @agent.default_options : @agent.options) %>
|
|
| 115 |
+ </textarea> |
|
| 116 |
+ </div> |
|
| 117 |
+ |
|
| 118 |
+ <div class="form-group"> |
|
| 119 |
+ <%= f.submit "Save", :class => "btn btn-primary" %> |
|
| 120 |
+ </div> |
|
| 117 | 121 |
</div> |
| 118 | 122 |
</div> |
| 119 | 123 |
</div> |
@@ -1,6 +1,6 @@ |
||
| 1 |
-<% if @agent.try(:oauthable?) %> |
|
| 1 |
+<% if agent.try(:oauthable?) %> |
|
| 2 | 2 |
<div class="form-group type-select"> |
| 3 | 3 |
<%= label_tag :service %> |
| 4 |
- <%= select_tag 'agent[service_id]', options_for_select(@agent.valid_services_for(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, @agent.service_id), class: 'form-control' %>
|
|
| 4 |
+ <%= select_tag 'agent[service_id]', options_for_select(agent.valid_services_for(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, agent.service_id), class: 'form-control' %>
|
|
| 5 | 5 |
</div> |
| 6 | 6 |
<% end %> |
@@ -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 %> |
@@ -1,8 +1,11 @@ |
||
| 1 |
-<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?sensor=false"></script> |
|
| 1 |
+<% content_for :head do -%> |
|
| 2 |
+<%= javascript_include_tag "https://maps.googleapis.com/maps/api/js?sensor=false" %> |
|
| 3 |
+<%= javascript_include_tag "map_marker" %> |
|
| 4 |
+<% end -%> |
|
| 2 | 5 |
|
| 3 | 6 |
<h3>Recent Event Map</h3> |
| 4 | 7 |
|
| 5 |
-<% events = @agent.events.where("lat IS NOT null AND lng IS NOT null").order("id desc").limit(500) %>
|
|
| 8 |
+<% events = @agent.events.with_location.order("id desc").limit(500) %>
|
|
| 6 | 9 |
<% if events.length > 0 %> |
| 7 | 10 |
<div id="map_canvas" style="width:800px; height:800px"></div> |
| 8 | 11 |
|
@@ -14,11 +17,10 @@ |
||
| 14 | 17 |
}; |
| 15 | 18 |
|
| 16 | 19 |
var map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions);
|
| 20 |
+ <% events.each do |event| %> |
|
| 21 |
+ map_marker(map, <%= Utils.jsonify(event.location) %>); |
|
| 22 |
+ <% end %> |
|
| 17 | 23 |
</script> |
| 18 |
- |
|
| 19 |
- <% events.each do |event| %> |
|
| 20 |
- <%= render "shared/map_marker", event: event %> |
|
| 21 |
- <% end %> |
|
| 22 | 24 |
<% else %> |
| 23 | 25 |
<p> |
| 24 | 26 |
No events found. |
@@ -15,7 +15,7 @@ |
||
| 15 | 15 |
<ul> |
| 16 | 16 |
<li>Read <a href="https://github.com/cantino/huginn/wiki/Run-Huginn-for-free-on-Heroku" target="_target">this document</a> carefully if you are going to try out Huginn for free on <a href="https://id.heroku.com/" target="_target">Heroku</a>.</li> |
| 17 | 17 |
|
| 18 |
- <li>Install the <a href="https://toolbelt.heroku.com/" target="_target">Heroku Toolbelt</a> and run <kbd>heroku login</kbd> if you haven't already.</li> |
|
| 18 |
+ <li>Install the <a href="https://toolbelt.heroku.com/" target="_target">Heroku Toolbelt</a> and run <kbd>heroku login</kbd>, if you haven't already.</li> |
|
| 19 | 19 |
|
| 20 | 20 |
<li>Run the following commands:<br /> |
| 21 | 21 |
<%= content_tag :pre do -%> |
@@ -25,7 +25,7 @@ bundle |
||
| 25 | 25 |
bin/setup_heroku |
| 26 | 26 |
<%- end %> |
| 27 | 27 |
|
| 28 |
- <li>Get back to this page and sign up with the invitation code shown by the last command.</li> |
|
| 28 |
+ <li>This command will create an admin account for you.</li> |
|
| 29 | 29 |
</ul> |
| 30 | 30 |
</div> |
| 31 | 31 |
<% end %> |
@@ -78,4 +78,4 @@ bin/setup_heroku |
||
| 78 | 78 |
</div> |
| 79 | 79 |
</div> |
| 80 | 80 |
</div> |
| 81 |
-</div> |
|
| 81 |
+</div> |
@@ -16,7 +16,10 @@ |
||
| 16 | 16 |
</p> |
| 17 | 17 |
|
| 18 | 18 |
<% if @event.lat && @event.lng %> |
| 19 |
- <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?sensor=false"></script> |
|
| 19 |
+ <% content_for :head do -%> |
|
| 20 |
+<%= javascript_include_tag "https://maps.googleapis.com/maps/api/js?sensor=false" %> |
|
| 21 |
+<%= javascript_include_tag "map_marker" %> |
|
| 22 |
+ <% end -%> |
|
| 20 | 23 |
|
| 21 | 24 |
<p> |
| 22 | 25 |
<b>Lat:</b> |
@@ -36,9 +39,9 @@ |
||
| 36 | 39 |
}; |
| 37 | 40 |
|
| 38 | 41 |
var map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions);
|
| 39 |
- </script> |
|
| 40 | 42 |
|
| 41 |
- <%= render "shared/map_marker", event: @event %> |
|
| 43 |
+ map_marker(map, <%= Utils.jsonify(@event.location) %>); |
|
| 44 |
+ </script> |
|
| 42 | 45 |
<% end %> |
| 43 | 46 |
|
| 44 | 47 |
<br /> |
@@ -28,7 +28,7 @@ |
||
| 28 | 28 |
|
| 29 | 29 |
<ul class="nav navbar-nav navbar-right"> |
| 30 | 30 |
<% if user_signed_in? %> |
| 31 |
- <form class="navbar-form navbar-left" role="search"> |
|
| 31 |
+ <form class="navbar-form navbar-left visible-lg" role="search"> |
|
| 32 | 32 |
<div class="form-group"> |
| 33 | 33 |
<input type="text" class="form-control" id='agent-navigate' autocomplete="off" placeholder="Search"> |
| 34 | 34 |
<%= image_tag "spinner-arrows.gif", :class => "spinner" %> |
@@ -36,22 +36,22 @@ |
||
| 36 | 36 |
</form> |
| 37 | 37 |
|
| 38 | 38 |
<li class='job-indicator' role='pending'> |
| 39 |
- <%= link_to current_user.admin? ? jobs_path : '#' do %> |
|
| 39 |
+ <%= link_to current_user.admin? ? jobs_path : '#', class: 'visible-lg' do %> |
|
| 40 | 40 |
<span class="badge"><span class="glyphicon glyphicon-refresh icon-white"></span> <span class='number'>0</span></span> |
| 41 | 41 |
<% end %> |
| 42 | 42 |
</li> |
| 43 | 43 |
<li class='job-indicator' role='awaiting_retry'> |
| 44 |
- <%= link_to current_user.admin? ? jobs_path : '#' do %> |
|
| 44 |
+ <%= link_to current_user.admin? ? jobs_path : '#', class: 'visible-lg' do %> |
|
| 45 | 45 |
<span class="badge"><span class="glyphicon glyphicon-question-sign icon-yellow"></span> <span class='number'>0</span></span> |
| 46 | 46 |
<% end %> |
| 47 | 47 |
</li> |
| 48 | 48 |
<li class='job-indicator' role='recent_failures'> |
| 49 |
- <%= link_to current_user.admin? ? jobs_path : '#' do %> |
|
| 49 |
+ <%= link_to current_user.admin? ? jobs_path : '#', class: 'hidden-sm hidden-xs' do %> |
|
| 50 | 50 |
<span class="badge"><span class="glyphicon glyphicon-exclamation-sign icon-white"></span> <span class='number'>0</span></span> |
| 51 | 51 |
<% end %> |
| 52 | 52 |
</li> |
| 53 | 53 |
<li id='event-indicator'> |
| 54 |
- <a href="#"> |
|
| 54 |
+ <a href="#" class='hidden-sm hidden-xs'> |
|
| 55 | 55 |
<span class="badge"><span class="glyphicon glyphicon-random icon-white"></span> <span class='number'>0</span> new events</span> |
| 56 | 56 |
</a> |
| 57 | 57 |
</li> |
@@ -35,22 +35,21 @@ |
||
| 35 | 35 |
</div> |
| 36 | 36 |
|
| 37 | 37 |
<script> |
| 38 |
- var agentPaths = {};
|
|
| 39 |
- var agentNames = []; |
|
| 38 |
+ window.agentPaths = {};
|
|
| 39 |
+ window.agentNames = []; |
|
| 40 | 40 |
<% if current_user.present? -%> |
| 41 | 41 |
var myAgents = <%= Utils.jsonify(current_user.agents.pluck(:name, :id).inject({}) {|m, a| m[a.first] = agent_path(a.last); m }) %>;
|
| 42 | 42 |
var myScenarios = <%= Utils.jsonify(current_user.scenarios.pluck(:name, :id).inject({}) {|m, s| m[s.first + " Scenario"] = scenario_path(s.last); m }) %>;
|
| 43 |
- $.extend(agentPaths, myAgents); |
|
| 44 |
- $.extend(agentPaths, myScenarios); |
|
| 45 |
- agentPaths["All Agents Index"] = <%= Utils.jsonify agents_path %>; |
|
| 46 |
- agentPaths["New Agent"] = <%= Utils.jsonify new_agent_path %>; |
|
| 47 |
- agentPaths["Account"] = <%= Utils.jsonify edit_user_registration_path %>; |
|
| 48 |
- agentPaths["Events Index"] = <%= Utils.jsonify events_path %>; |
|
| 49 |
- agentPaths["View Agent Diagram"] = <%= Utils.jsonify diagram_path %>; |
|
| 50 |
- agentPaths["Run Event Propagation"] = { url: <%= Utils.jsonify propagate_agents_path %>, method: 'POST' };
|
|
| 43 |
+ $.extend(window.agentPaths, myAgents); |
|
| 44 |
+ $.extend(window.agentPaths, myScenarios); |
|
| 45 |
+ window.agentPaths["All Agents Index"] = <%= Utils.jsonify agents_path %>; |
|
| 46 |
+ window.agentPaths["New Agent"] = <%= Utils.jsonify new_agent_path %>; |
|
| 47 |
+ window.agentPaths["Account"] = <%= Utils.jsonify edit_user_registration_path %>; |
|
| 48 |
+ window.agentPaths["Events Index"] = <%= Utils.jsonify events_path %>; |
|
| 49 |
+ window.agentPaths["View Agent Diagram"] = <%= Utils.jsonify diagram_path %>; |
|
| 50 |
+ window.agentPaths["Run Event Propagation"] = { url: <%= Utils.jsonify propagate_agents_path %>, method: 'POST' };
|
|
| 51 | 51 |
|
| 52 |
- |
|
| 53 |
- $.each(agentPaths, function(name, v) { agentNames.push(name); });
|
|
| 52 |
+ $.each(window.agentPaths, function(name, v) { window.agentNames.push(name); });
|
|
| 54 | 53 |
<% end -%> |
| 55 | 54 |
</script> |
| 56 | 55 |
</body> |
@@ -11,14 +11,14 @@ |
||
| 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') %>
|
|
| 15 |
- <p><%= link_to "Authenticate with Twitter", "/auth/twitter" %></p> |
|
| 14 |
+ <% if has_oauth_configuration_for?('twitter') %>
|
|
| 15 |
+ <p><%= link_to "/auth/twitter", class: 'btn btn-default btn-auth btn-auth-twitter' do %><i class='fa fa-twitter'></i><span>Authenticate with Twitter</span><% end %></p> |
|
| 16 | 16 |
<% end %> |
| 17 |
- <% if has_oauth_configuration_for('thirty_seven_signals') %>
|
|
| 18 |
- <p><%= link_to "Authenticate with 37Signals (Basecamp)", "/auth/37signals" %></p> |
|
| 17 |
+ <% if has_oauth_configuration_for?('37signals') %>
|
|
| 18 |
+ <p><%= link_to "/auth/37signals", class: 'btn btn-default btn-auth btn-auth-37signals' do %><i class='fa fa-lock'></i><span>Authenticate with 37Signals (Basecamp)</span><% end %></p> |
|
| 19 | 19 |
<% end -%> |
| 20 |
- <% if has_oauth_configuration_for('github') %>
|
|
| 21 |
- <p><%= link_to "Authenticate with Github", "/auth/github" %></p> |
|
| 20 |
+ <% if has_oauth_configuration_for?('github') %>
|
|
| 21 |
+ <p><%= link_to "/auth/github", class: 'btn btn-default btn-auth btn-auth-github' do %><i class='fa fa-github'></i><span>Authenticate with Github</span><% end %></p> |
|
| 22 | 22 |
<% end -%> |
| 23 | 23 |
<% if has_oauth_configuration_for('tumblr') %>
|
| 24 | 24 |
<p><%= link_to "Authenticate with Tumblr", "/auth/tumblr" %></p> |
@@ -1,61 +0,0 @@ |
||
| 1 |
-<script> |
|
| 2 |
- (function(map) {
|
|
| 3 |
- <% |
|
| 4 |
- if event.payload[:horizontal_accuracy] && event.payload[:vertical_accuracy] |
|
| 5 |
- radius = (event.payload[:horizontal_accuracy].to_f + event.payload[:vertical_accuracy].to_f) / 2.0 |
|
| 6 |
- elsif event.payload[:horizontal_accuracy] |
|
| 7 |
- radius = event.payload[:horizontal_accuracy].to_f |
|
| 8 |
- elsif event.payload[:vertical_accuracy] |
|
| 9 |
- radius = event.payload[:vertical_accuracy].to_f |
|
| 10 |
- elsif event.payload[:accuracy] |
|
| 11 |
- radius = event.payload[:accuracy].to_f |
|
| 12 |
- else |
|
| 13 |
- radius = 0 |
|
| 14 |
- end |
|
| 15 |
- %> |
|
| 16 |
- |
|
| 17 |
- var pos = new google.maps.LatLng(<%= event.lat %>, <%= event.lng %>); |
|
| 18 |
- |
|
| 19 |
- <% if radius > 0 %> |
|
| 20 |
- var accuracyCircle = new google.maps.Circle({
|
|
| 21 |
- strokeColor: '#FF0000', |
|
| 22 |
- strokeOpacity: 0.8, |
|
| 23 |
- strokeWeight: 2, |
|
| 24 |
- fillColor: '#FF0000', |
|
| 25 |
- fillOpacity: 0.35, |
|
| 26 |
- map: map, |
|
| 27 |
- center: pos, |
|
| 28 |
- radius: <%= radius %> |
|
| 29 |
- }); |
|
| 30 |
- <% else %> |
|
| 31 |
- var marker = new google.maps.Marker({
|
|
| 32 |
- position: pos, |
|
| 33 |
- map: map, |
|
| 34 |
- title: 'Recorded Location' |
|
| 35 |
- }); |
|
| 36 |
- <% end %> |
|
| 37 |
- |
|
| 38 |
- |
|
| 39 |
- <% if event.payload[:course] && event.payload[:course].to_f > -1 %> |
|
| 40 |
- var p1 = new LatLon(pos.lat(), pos.lng()); |
|
| 41 |
- var p2 = p1.destinationPoint(<%= event.payload[:course].to_f %>, <%= [0.2, (event.payload[:speed] || 1).to_f].max * 0.1 %>); |
|
| 42 |
- |
|
| 43 |
- var lineCoordinates = [ pos, new google.maps.LatLng(p2.lat(), p2.lon()) ]; |
|
| 44 |
- |
|
| 45 |
- var lineSymbol = {
|
|
| 46 |
- path:google.maps.SymbolPath.FORWARD_CLOSED_ARROW |
|
| 47 |
- }; |
|
| 48 |
- |
|
| 49 |
- var line = new google.maps.Polyline({
|
|
| 50 |
- path: lineCoordinates, |
|
| 51 |
- icons: [ |
|
| 52 |
- {
|
|
| 53 |
- icon: lineSymbol, |
|
| 54 |
- offset: '100%' |
|
| 55 |
- } |
|
| 56 |
- ], |
|
| 57 |
- map: map |
|
| 58 |
- }); |
|
| 59 |
- <% end %> |
|
| 60 |
- })(map); |
|
| 61 |
-</script> |
@@ -37,8 +37,9 @@ |
||
| 37 | 37 |
<br/> |
| 38 | 38 |
|
| 39 | 39 |
<div class="btn-group"> |
| 40 |
- <%= link_to '<span class="glyphicon glyphicon-plus"></span> New Credential'.html_safe, new_user_credential_path, class: "btn btn-default" %> |
|
| 40 |
+ <%= link_to new_user_credential_path, class: "btn btn-default" do %><span class="glyphicon glyphicon-plus"></span> New Credential<% end %> |
|
| 41 |
+ <%= link_to user_credentials_path(format: :json), class: "btn btn-default" do %><span class="glyphicon glyphicon-download-alt"></span> Download Credentials<% end %> |
|
| 41 | 42 |
</div> |
| 42 | 43 |
</div> |
| 43 | 44 |
</div> |
| 44 |
-</div> |
|
| 45 |
+</div> |
@@ -138,19 +138,6 @@ unless $config['SMTP_DOMAIN'] && $config['SMTP_USER_NAME'] && $config['SMTP_PASS |
||
| 138 | 138 |
end |
| 139 | 139 |
end |
| 140 | 140 |
|
| 141 |
-if first_time |
|
| 142 |
- puts "Restarting..." |
|
| 143 |
- puts capture("heroku restart")
|
|
| 144 |
- |
|
| 145 |
- puts "Done!" |
|
| 146 |
- puts |
|
| 147 |
- puts "Visit https://#{app_name}.herokuapp.com/users/sign_up and use the invitation code shown below:"
|
|
| 148 |
- puts |
|
| 149 |
- puts "\t#{$config['INVITATION_CODE']}"
|
|
| 150 |
- |
|
| 151 |
- exit |
|
| 152 |
-end |
|
| 153 |
- |
|
| 154 | 141 |
branch = capture("git rev-parse --abbrev-ref HEAD")
|
| 155 | 142 |
if yes?("Should I push your current branch (#{branch}) to heroku?")
|
| 156 | 143 |
puts "This may take a moment..." |
@@ -158,21 +145,44 @@ if yes?("Should I push your current branch (#{branch}) to heroku?")
|
||
| 158 | 145 |
|
| 159 | 146 |
puts "Running database migrations..." |
| 160 | 147 |
puts capture("heroku run rake db:migrate")
|
| 148 |
+end |
|
| 161 | 149 |
|
| 150 |
+if first_time |
|
| 151 |
+ puts "Restarting..." |
|
| 152 |
+ puts capture("heroku restart")
|
|
| 153 |
+ puts "Done!" |
|
| 162 | 154 |
puts |
| 163 | 155 |
puts |
| 164 | 156 |
puts "I can make an admin user on your new Huginn instance and setup some example Agents." |
| 165 | 157 |
if yes?("Should I create a new admin user and some example Agents?")
|
| 166 |
- seed_email = nag "Okay, what is your email address?" |
|
| 167 |
- seed_username = nag "And what username would you like to login as?" |
|
| 168 |
- seed_password = nag "Finally, what password would you like to use?", noecho: true |
|
| 169 |
- puts "\nJust a moment..." |
|
| 170 |
- |
|
| 171 |
- capture("heroku run rake db:seed SEED_EMAIL=#{seed_email} SEED_USERNAME=#{seed_username} SEED_PASSWORD=#{seed_password}")
|
|
| 158 |
+ done = false |
|
| 159 |
+ while !done |
|
| 160 |
+ seed_email = nag "Okay, what is your email address?" |
|
| 161 |
+ seed_username = nag "And what username would you like to login as?" |
|
| 162 |
+ seed_password = nag "Finally, what password would you like to use?", noecho: true |
|
| 163 |
+ puts "\nJust a moment..." |
|
| 164 |
+ |
|
| 165 |
+ result = capture("heroku run rake db:seed SEED_EMAIL=#{seed_email} SEED_USERNAME=#{seed_username} SEED_PASSWORD=#{seed_password}")
|
|
| 166 |
+ if result =~ /Validation failed/ |
|
| 167 |
+ puts "ERROR:" |
|
| 168 |
+ puts |
|
| 169 |
+ puts result |
|
| 170 |
+ puts |
|
| 171 |
+ else |
|
| 172 |
+ done = true |
|
| 173 |
+ end |
|
| 174 |
+ end |
|
| 172 | 175 |
puts |
| 173 | 176 |
puts |
| 174 | 177 |
puts "Okay, you should be all set! Visit https://#{app_name}.herokuapp.com and login as '#{seed_username}' with your password."
|
| 178 |
+ puts |
|
| 179 |
+ puts "If you'd like to make more users, you can visit https://#{app_name}.herokuapp.com/users/sign_up and use the invitation code:"
|
|
| 180 |
+ else |
|
| 181 |
+ puts |
|
| 182 |
+ puts "Visit https://#{app_name}.herokuapp.com/users/sign_up and use the invitation code shown below:"
|
|
| 175 | 183 |
end |
| 184 |
+ puts |
|
| 185 |
+ puts "\t#{$config['INVITATION_CODE']}"
|
|
| 176 | 186 |
end |
| 177 | 187 |
|
| 178 | 188 |
puts |
@@ -1,6 +1,9 @@ |
||
| 1 | 1 |
require 'thread' |
| 2 | 2 |
require 'huginn_scheduler' |
| 3 | 3 |
|
| 4 |
+STDOUT.sync = true |
|
| 5 |
+STDERR.sync = true |
|
| 6 |
+ |
|
| 4 | 7 |
def stop |
| 5 | 8 |
puts 'Exiting...' |
| 6 | 9 |
@scheduler.stop |
@@ -14,6 +17,7 @@ def safely(&block) |
||
| 14 | 17 |
rescue StandardError => e |
| 15 | 18 |
STDERR.puts "\nException #{e.message}:\n#{e.backtrace.join("\n")}\n\n"
|
| 16 | 19 |
STDERR.puts "Terminating myself ..." |
| 20 |
+ STDERR.flush |
|
| 17 | 21 |
stop |
| 18 | 22 |
end |
| 19 | 23 |
end |
@@ -61,7 +61,7 @@ Huginn::Application.configure do |
||
| 61 | 61 |
end |
| 62 | 62 |
|
| 63 | 63 |
# Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added) |
| 64 |
- config.assets.precompile += %w( diagram.js graphing.js user_credentials.js ) |
|
| 64 |
+ config.assets.precompile += %w( diagram.js graphing.js map_marker.js user_credentials.js ) |
|
| 65 | 65 |
|
| 66 | 66 |
# Ignore bad email addresses and do not raise email delivery errors. |
| 67 | 67 |
# Set this to true and configure the email server for immediate delivery to raise delivery errors. |
@@ -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,6 +1,43 @@ |
||
| 1 |
+OMNIAUTH_PROVIDERS = {}.tap { |providers|
|
|
| 2 |
+ if defined?(OmniAuth::Strategies::Twitter) && |
|
| 3 |
+ (key = ENV["TWITTER_OAUTH_KEY"]).present? && |
|
| 4 |
+ (secret = ENV["TWITTER_OAUTH_SECRET"]).present? |
|
| 5 |
+ providers['twitter'] = {
|
|
| 6 |
+ omniauth_params: [key, secret, authorize_params: {force_login: 'true', use_authorize: 'true'}]
|
|
| 7 |
+ } |
|
| 8 |
+ end |
|
| 9 |
+ |
|
| 10 |
+ if defined?(OmniAuth::Strategies::ThirtySevenSignals) && |
|
| 11 |
+ (key = ENV["THIRTY_SEVEN_SIGNALS_OAUTH_KEY"]).present? && |
|
| 12 |
+ (secret = ENV["THIRTY_SEVEN_SIGNALS_OAUTH_SECRET"]).present? |
|
| 13 |
+ providers['37signals'] = {
|
|
| 14 |
+ omniauth_params: [key, secret] |
|
| 15 |
+ } |
|
| 16 |
+ end |
|
| 17 |
+ |
|
| 18 |
+ if defined?(OmniAuth::Strategies::GitHub) && |
|
| 19 |
+ (key = ENV["GITHUB_OAUTH_KEY"]).present? && |
|
| 20 |
+ (secret = ENV["GITHUB_OAUTH_SECRET"]).present? |
|
| 21 |
+ providers['github'] = {
|
|
| 22 |
+ omniauth_params: [key, secret] |
|
| 23 |
+ } |
|
| 24 |
+ end |
|
| 25 |
+ |
|
| 26 |
+ if defined?(OmniAuth::Strategies::Tumblr) && |
|
| 27 |
+ (key = ENV["TUMBLR_OAUTH_KEY"]).present? && |
|
| 28 |
+ (secret = ENV["TUMBLR_OAUTH_SECRET"]).present? |
|
| 29 |
+ providers['tumblr'] = {
|
|
| 30 |
+ omniauth_params: [key, secret] |
|
| 31 |
+ } |
|
| 32 |
+ end |
|
| 33 |
+} |
|
| 34 |
+ |
|
| 35 |
+def has_oauth_configuration_for?(provider) |
|
| 36 |
+ OMNIAUTH_PROVIDERS.key?(provider.to_s) |
|
| 37 |
+end |
|
| 38 |
+ |
|
| 1 | 39 |
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'] |
|
| 5 |
- provider :tumblr, ENV['TUMBLR_OAUTH_KEY'], ENV['TUMBLR_OAUTH_SECRET'] |
|
| 40 |
+ OMNIAUTH_PROVIDERS.each { |name, config|
|
|
| 41 |
+ provider name, *config[:omniauth_params] |
|
| 42 |
+ } |
|
| 6 | 43 |
end |
@@ -0,0 +1,30 @@ |
||
| 1 |
+FROM ubuntu:14.04 |
|
| 2 |
+MAINTAINER Andrew Cantino |
|
| 3 |
+ |
|
| 4 |
+ENV DEBIAN_FRONTEND noninteractive |
|
| 5 |
+RUN apt-get update && \ |
|
| 6 |
+ apt-get install -y software-properties-common && \ |
|
| 7 |
+ add-apt-repository -y ppa:git-core/ppa && \ |
|
| 8 |
+ add-apt-repository -y ppa:brightbox/ruby-ng && \ |
|
| 9 |
+ apt-get update && \ |
|
| 10 |
+ apt-get install -y build-essential checkinstall postgresql-client \ |
|
| 11 |
+ git-core mysql-server redis-server python2.7 python-docutils \ |
|
| 12 |
+ libmysqlclient-dev libpq-dev zlib1g-dev libyaml-dev libssl-dev \ |
|
| 13 |
+ libgdbm-dev libreadline-dev libncurses5-dev libffi-dev \ |
|
| 14 |
+ libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev \ |
|
| 15 |
+ graphviz libgraphviz-dev \ |
|
| 16 |
+ ruby2.1 ruby2.1-dev supervisor && \ |
|
| 17 |
+ gem install --no-ri --no-rdoc bundler && \ |
|
| 18 |
+ rm -rf /var/lib/apt/lists/* |
|
| 19 |
+ |
|
| 20 |
+ADD scripts/ /scripts |
|
| 21 |
+RUN chmod 755 /scripts/setup /scripts/init |
|
| 22 |
+ |
|
| 23 |
+RUN /scripts/setup |
|
| 24 |
+ |
|
| 25 |
+VOLUME /var/lib/mysql |
|
| 26 |
+ |
|
| 27 |
+EXPOSE 5000 |
|
| 28 |
+ |
|
| 29 |
+CMD ["/scripts/init"] |
|
| 30 |
+ |
@@ -0,0 +1,2 @@ |
||
| 1 |
+build: |
|
| 2 |
+ docker build -t cantino/huginn . |
@@ -0,0 +1,137 @@ |
||
| 1 |
+Huginn for docker with multiple container linkage |
|
| 2 |
+================================================= |
|
| 3 |
+ |
|
| 4 |
+This image runs a linkable [Huginn](https://github.com/cantino/huginn) instance. |
|
| 5 |
+ |
|
| 6 |
+There is an automated build repository on docker hub for [cantino/huginn](https://registry.hub.docker.com/builds/github/cantino/huginn/). |
|
| 7 |
+ |
|
| 8 |
+This was patterned after [sameersbn/gitlab](https://registry.hub.docker.com/u/sameersbn/gitlab) by [ianblenke/huginn](http://github.com/ianblenke/huginn), and imported here for official generation of a docker hub auto-build image. |
|
| 9 |
+ |
|
| 10 |
+The scripts/init script generates a .env file containing the variables as passed as per normal Huginn documentation. |
|
| 11 |
+The same environment variables that would be used for Heroku PaaS deployment are used by this script. |
|
| 12 |
+ |
|
| 13 |
+The scripts/init script is aware of mysql and postgres linked containers through the environment variables: |
|
| 14 |
+ |
|
| 15 |
+ MYSQL_PORT_3306_TCP_ADDR |
|
| 16 |
+ MYSQL_PORT_3306_TCP_PORT |
|
| 17 |
+ |
|
| 18 |
+and |
|
| 19 |
+ |
|
| 20 |
+ POSTGRESQL_PORT_5432_TCP_ADDR |
|
| 21 |
+ POSTGRESQL_PORT_5432_TCP_PORT |
|
| 22 |
+ |
|
| 23 |
+Its recommended to use an image that allows you to create a database via environmental variables at docker run, like `paintedfox / postgresql` or `centurylink / mysql`, so the db is populated when this script runs. |
|
| 24 |
+ |
|
| 25 |
+If you do not link a database container, a built-in mysql database will be started. |
|
| 26 |
+There is an exported docker volume of /var/lib/mysql to allow persistence of that mysql database. |
|
| 27 |
+ |
|
| 28 |
+Additionally, the database variables may be overridden from the above as per the standard Huginn documentation: |
|
| 29 |
+ |
|
| 30 |
+ HUGINN_DATABASE_ADAPTER #(must be either 'postgres' or 'mysql2') |
|
| 31 |
+ HUGINN_DATABASE_HOST |
|
| 32 |
+ HUGINN_DATABASE_PORT |
|
| 33 |
+ |
|
| 34 |
+This script will run database migrations (rake db:migrate) which should be idempotent. |
|
| 35 |
+ |
|
| 36 |
+It will also seed the database (rake db:seed) unless this is defined: |
|
| 37 |
+ |
|
| 38 |
+ DO_NOT_SEED |
|
| 39 |
+ |
|
| 40 |
+This same seeding initially defines the "admin" user with a default password of "password" as per the standard Huginn documentation. |
|
| 41 |
+ |
|
| 42 |
+If you do not wish to have the default 6 agents, you will want to set the above environment variable after your initially deploy, otherwise they will be added automatically the next time a container pointing at the database is spun up. |
|
| 43 |
+ |
|
| 44 |
+The CMD launches Huginn via the scripts/init script. This may become the ENTRYPOINT later. It does take under a minute for Huginn to come up. Use environmental variables that match your DB's creds to ensure it works. |
|
| 45 |
+ |
|
| 46 |
+## Usage |
|
| 47 |
+ |
|
| 48 |
+Simple stand-alone usage: |
|
| 49 |
+ |
|
| 50 |
+ docker run -it -p 5000:5000 cantino/huginn |
|
| 51 |
+ |
|
| 52 |
+To link to another mysql container, for example: |
|
| 53 |
+ |
|
| 54 |
+ docker run --rm --name newcentury_mysql -p 3306 \ |
|
| 55 |
+ -e HUGINN_MYSQL_DATABASE=huginn \ |
|
| 56 |
+ -e HUGINN_MYSQL_USER=huginn \ |
|
| 57 |
+ -e HUGINN_MYSQL_PASSWORD=somethingsecret \ |
|
| 58 |
+ -e HUGINN_MYSQL_ROOT_PASSWORD=somethingevenmoresecret \ |
|
| 59 |
+ cantino/huginn |
|
| 60 |
+ docker run --rm --name huginn --link newcentury_mysql:MYSQL -p 5000:5000 \ |
|
| 61 |
+ -e HUGINN_DATABASE_NAME=huginn \ |
|
| 62 |
+ -e HUGINN_DATABASE_USER=huginn \ |
|
| 63 |
+ -e HUGINN_DATABASE_PASSWORD=somethingsecret \ |
|
| 64 |
+ cantino/huginn |
|
| 65 |
+ |
|
| 66 |
+To link to another container named 'postgres': |
|
| 67 |
+ |
|
| 68 |
+ docker run --rm --name huginn --link POSTGRES:mysql -p 5000:5000 -e "DATABASE_USER=huginn" -e "DATABASE_PASSWORD=pass@word" cantino/huginn |
|
| 69 |
+ |
|
| 70 |
+## Environment Variables |
|
| 71 |
+ |
|
| 72 |
+Other Huginn 12factored environment variables of note, as generated and put into the .env file as per Huginn documentation, |
|
| 73 |
+with an additional `HUGINN_` prefix to the variable. |
|
| 74 |
+ |
|
| 75 |
+These are: |
|
| 76 |
+ |
|
| 77 |
+ HUGINN_APP_SECRET_TOKEN |
|
| 78 |
+ HUGINN_DOMAIN |
|
| 79 |
+ HUGINN_ASSET_HOST |
|
| 80 |
+ HUGINN_DATABASE_ADAPTER |
|
| 81 |
+ HUGINN_DATABASE_ENCODING |
|
| 82 |
+ HUGINN_DATABASE_RECONNECT |
|
| 83 |
+ HUGINN_DATABASE_NAME |
|
| 84 |
+ HUGINN_DATABASE_POOL |
|
| 85 |
+ HUGINN_DATABASE_USERNAME |
|
| 86 |
+ HUGINN_DATABASE_PASSWORD |
|
| 87 |
+ HUGINN_DATABASE_HOST |
|
| 88 |
+ HUGINN_DATABASE_PORT |
|
| 89 |
+ HUGINN_DATABASE_SOCKET |
|
| 90 |
+ HUGINN_RAILS_ENV |
|
| 91 |
+ HUGINN_FORCE_SSL |
|
| 92 |
+ HUGINN_INVITATION_CODE |
|
| 93 |
+ HUGINN_SMTP_DOMAIM |
|
| 94 |
+ HUGINN_SMTP_USER_NAME |
|
| 95 |
+ HUGINN_SMTP_PASSWORD |
|
| 96 |
+ HUGINN_SMTP_SERVER |
|
| 97 |
+ HUGINN_SMTP_PORT |
|
| 98 |
+ HUGINN_SMTP_AUTHENTICATION |
|
| 99 |
+ HUGINN_SMTP_ENABLE_STARTTLS_AUTO |
|
| 100 |
+ HUGINN_EMAIL_FROM_ADDRESS |
|
| 101 |
+ HUGINN_AGENT_LOG_LENGTH |
|
| 102 |
+ HUGINN_TWITTER_OAUTH_KEY |
|
| 103 |
+ HUGINN_TWITTER_OAUTH_SECRET |
|
| 104 |
+ HUGINN_THIRTY_SEVEN_SIGNALS_OAUTH_KEY |
|
| 105 |
+ HUGINN_THIRTY_SEVEN_SIGNALS_OAUTH_SECRET |
|
| 106 |
+ HUGINN_GITHUB_OAUTH_KEY |
|
| 107 |
+ HUGINN_GITHUB_OAUTH_SECRET |
|
| 108 |
+ HUGINN_AWS_ACCESS_KEY_ID |
|
| 109 |
+ HUGINN_AWS_ACCESS_KEY |
|
| 110 |
+ HUGINN_AWS_SANDBOX |
|
| 111 |
+ HUGINN_FARADAY_HTTP_BACKEND |
|
| 112 |
+ HUGINN_DEFAULT_HTTP_USER_AGENT |
|
| 113 |
+ HUGINN_ALLOW_JSONPATH_EVAL |
|
| 114 |
+ HUGINN_ENABLE_INSECURE_AGENTS |
|
| 115 |
+ HUGGIN_ENABLE_SECOND_PRECISION_SCHEDULE |
|
| 116 |
+ HUGINN_USE_GRAPHVIZ_DOT |
|
| 117 |
+ HUGINN_TIMEZONE |
|
| 118 |
+ HUGGIN_FAILED_JOBS_TO_KEEP |
|
| 119 |
+ |
|
| 120 |
+ |
|
| 121 |
+The above environment variables will override the defaults. The defaults are read from the [.env.example](https://github.com/cantino/huginn/blob/master/.env.example) file. |
|
| 122 |
+ |
|
| 123 |
+For variables in the .env.example that are commented out, the default is to not include that variable in the generated .env file. |
|
| 124 |
+ |
|
| 125 |
+## Building on your own |
|
| 126 |
+ |
|
| 127 |
+You don't need to do this on your own, because there is an [automated build](https://registry.hub.docker.com/u/cantino/huginn/) for this repository, but if you really want: |
|
| 128 |
+ |
|
| 129 |
+ docker build --rm=true --tag={yourname}/huginn .
|
|
| 130 |
+ |
|
| 131 |
+## Source |
|
| 132 |
+ |
|
| 133 |
+The source is [available on GitHub](https://github.com/cantino/huginn/). |
|
| 134 |
+ |
|
| 135 |
+Please feel free to submit pull requests and/or fork at your leisure. |
|
| 136 |
+ |
|
| 137 |
+ |
@@ -0,0 +1,111 @@ |
||
| 1 |
+#!/bin/bash |
|
| 2 |
+set -e |
|
| 3 |
+ |
|
| 4 |
+cd /app |
|
| 5 |
+ |
|
| 6 |
+# Default to the environment variable values set in .env.example |
|
| 7 |
+source /app/.env.example |
|
| 8 |
+ |
|
| 9 |
+# is a mysql or postgresql database linked? |
|
| 10 |
+# requires that the mysql or postgresql containers have exposed |
|
| 11 |
+# port 3306 and 5432 respectively. |
|
| 12 |
+if [ -n "${MYSQL_PORT_3306_TCP_ADDR}" ]; then
|
|
| 13 |
+ HUGINN_DATABASE_ADAPTER=${HUGINN_DATABASE_ADAPTER:-mysql2}
|
|
| 14 |
+ HUGINN_DATABASE_HOST=${HUGINN_DATABASE_HOST:-${MYSQL_PORT_3306_TCP_ADDR}}
|
|
| 15 |
+ HUGINN_DATABASE_PORT=${HUGINN_DATABASE_PORT:-${MYSQL_PORT_3306_TCP_PORT}}
|
|
| 16 |
+elif [ -n "${POSTGRESQL_PORT_5432_TCP_ADDR}" ]; then
|
|
| 17 |
+ HUGINN_DATABASE_ADAPTER=${HUGINN_DATABASE_ADAPTER:-postgres}
|
|
| 18 |
+ HUGINN_DATABASE_HOST=${HUGINN_DATABASE_HOST:-${POSTGRESQL_PORT_5432_TCP_ADDR}}
|
|
| 19 |
+ HUGINN_DATABASE_PORT=${HUGINN_DATABASE_PORT:-${POSTGRESQL_PORT_5432_TCP_PORT}}
|
|
| 20 |
+fi |
|
| 21 |
+ |
|
| 22 |
+grep = /app/.env.example | sed -e 's/^#[^ ]//' | grep -v -e '^#' | cut -d= -f1 | \ |
|
| 23 |
+ while read var ; do |
|
| 24 |
+ eval "echo \"$var=\\\"\${HUGINN_$var:-\$$var}\\\"\""
|
|
| 25 |
+ done | grep -v -e ^= > /app/.env |
|
| 26 |
+ |
|
| 27 |
+chmod ugo+r /app/.env |
|
| 28 |
+source /app/.env |
|
| 29 |
+ |
|
| 30 |
+DATABASE_HOST=${HUGINN_DATABASE_HOST:-${DATABASE_HOST:-localhost}}
|
|
| 31 |
+DATABASE_ENCODING=${HUGINN_DATABASE_ENCODING:-${DATABASE_ENCODING}}
|
|
| 32 |
+USE_GRAPHVIZ_DOT=${HUGINN_USE_GRAPHVIZ_DOT:-${USE_GRAPHVIZ_DOT}}
|
|
| 33 |
+ |
|
| 34 |
+# use default port number if it is still not set |
|
| 35 |
+case "${DATABASE_ADAPTER}" in
|
|
| 36 |
+ mysql2) DATABASE_PORT=${DATABASE_PORT:-3306} ;;
|
|
| 37 |
+ postgres) DATABASE_PORT=${DATABASE_PORT:-5432} ;;
|
|
| 38 |
+ *) echo "Unsupported database adapter. Available adapters are mysql2, and postgres." && exit 1 ;; |
|
| 39 |
+esac |
|
| 40 |
+ |
|
| 41 |
+# start supervisord |
|
| 42 |
+/usr/bin/supervisord -c /etc/supervisor/supervisord.conf |
|
| 43 |
+ |
|
| 44 |
+# start mysql server if ${DATABASE_HOST} is localhost
|
|
| 45 |
+if [ "${DATABASE_HOST}" == "localhost" ]; then
|
|
| 46 |
+ if [ "${DATABASE_ADAPTER}" == "postgres" ]; then
|
|
| 47 |
+ echo "DATABASE_ADAPTER 'postgres' is not supported internally. Please provide DATABASE_HOST." |
|
| 48 |
+ exit 1 |
|
| 49 |
+ fi |
|
| 50 |
+ |
|
| 51 |
+ # configure supervisord to start mysql (manual) |
|
| 52 |
+ cat > /etc/supervisor/conf.d/mysqld.conf <<EOF |
|
| 53 |
+[program:mysqld] |
|
| 54 |
+priority=20 |
|
| 55 |
+directory=/tmp |
|
| 56 |
+command=/usr/bin/mysqld_safe |
|
| 57 |
+user=root |
|
| 58 |
+autostart=false |
|
| 59 |
+autorestart=true |
|
| 60 |
+stdout_logfile=/var/log/supervisor/%(program_name)s.log |
|
| 61 |
+stderr_logfile=/var/log/supervisor/%(program_name)s.log |
|
| 62 |
+EOF |
|
| 63 |
+ supervisorctl reload |
|
| 64 |
+ |
|
| 65 |
+ # fix permissions and ownership of /var/lib/mysql |
|
| 66 |
+ chown -R mysql:mysql /var/lib/mysql |
|
| 67 |
+ chmod 700 /var/lib/mysql |
|
| 68 |
+ |
|
| 69 |
+ # initialize MySQL data directory |
|
| 70 |
+ if [ ! -d /var/lib/mysql/mysql ]; then |
|
| 71 |
+ mysql_install_db --user=mysql |
|
| 72 |
+ fi |
|
| 73 |
+ |
|
| 74 |
+ echo "Starting mysql server..." |
|
| 75 |
+ supervisorctl start mysqld >/dev/null |
|
| 76 |
+ |
|
| 77 |
+ # wait for mysql server to start (max 120 seconds) |
|
| 78 |
+ timeout=120 |
|
| 79 |
+ while ! mysqladmin -uroot ${DATABASE_PASSWORD:+-p$DATABASE_PASSWORD} status >/dev/null 2>&1
|
|
| 80 |
+ do |
|
| 81 |
+ timeout=$(expr $timeout - 1) |
|
| 82 |
+ if [ $timeout -eq 0 ]; then |
|
| 83 |
+ echo "Failed to start mysql server" |
|
| 84 |
+ exit 1 |
|
| 85 |
+ fi |
|
| 86 |
+ sleep 1 |
|
| 87 |
+ done |
|
| 88 |
+ |
|
| 89 |
+ if ! echo "USE ${DATABASE_NAME}" | mysql -uroot ${DATABASE_PASSWORD:+-p$DATABASE_PASSWORD} >/dev/null 2>&1; then
|
|
| 90 |
+ DB_INIT="yes" |
|
| 91 |
+ echo "CREATE DATABASE IF NOT EXISTS \`${DATABASE_NAME}\` DEFAULT CHARACTER SET \`utf8\` COLLATE \`utf8_unicode_ci\`;" | mysql -uroot ${DATABASE_PASSWORD:+-p$DATABASE_PASSWORD}
|
|
| 92 |
+ echo "GRANT SELECT, LOCK TABLES, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER ON \`${DATABASE_NAME}\`.* TO 'root'@'localhost';" | mysql -uroot ${DATABASE_PASSWORD:+-p$DATABASE_PASSWORD}
|
|
| 93 |
+ fi |
|
| 94 |
+fi |
|
| 95 |
+ |
|
| 96 |
+# Assuming we have a created database, run the migrations and seed it idempotently. |
|
| 97 |
+[ -z "${DO_NOT_MIGRATE}" ] && sudo -u huginn -EH bundle exec rake db:migrate
|
|
| 98 |
+[ -z "${DO_NOT_SEED}" ] && sudo -u huginn -EH bundle exec rake db:seed
|
|
| 99 |
+ |
|
| 100 |
+[ -n "$INTENTIONALLY_SLEEP" ] && sleep $INTENTIONALLY_SLEEP |
|
| 101 |
+ |
|
| 102 |
+# Fixup the Procfile and prepare the PORT |
|
| 103 |
+[ -z "${DO_NOT_RUN_JOBS}" ] && perl -pi -e 's/^jobs:/#jobs:/' /app/Procfile
|
|
| 104 |
+perl -pi -e 's/rails server$/rails server -p \$PORT/' /app/Procfile |
|
| 105 |
+export PORT |
|
| 106 |
+ |
|
| 107 |
+# Start huginn |
|
| 108 |
+sudo -u huginn -EH bundle exec foreman start |
|
| 109 |
+ |
|
| 110 |
+# As the ENTRYPOINT script, when this exits the docker container will Exit. |
|
| 111 |
+exit 0 |
@@ -0,0 +1,39 @@ |
||
| 1 |
+#!/bin/bash |
|
| 2 |
+set -e |
|
| 3 |
+ |
|
| 4 |
+# Initialize variables used by Huginn at installation time |
|
| 5 |
+ |
|
| 6 |
+# Huginn is 12factor aware, embrace that fact for use inside of docker |
|
| 7 |
+ON_HEROKU=${ON_HEROKU:-true}
|
|
| 8 |
+ |
|
| 9 |
+# Shallow clone the huginn project repo |
|
| 10 |
+git clone --depth 1 https://github.com/cantino/huginn /app |
|
| 11 |
+ |
|
| 12 |
+cd /app |
|
| 13 |
+ |
|
| 14 |
+# add a huginn group and user |
|
| 15 |
+adduser --group huginn |
|
| 16 |
+adduser --disabled-login --ingroup huginn --gecos 'Huginn' --no-create-home --home /app huginn |
|
| 17 |
+adduser huginn sudo |
|
| 18 |
+passwd -d huginn |
|
| 19 |
+ |
|
| 20 |
+# Change the ownership to huginn |
|
| 21 |
+chown -R huginn:huginn /app |
|
| 22 |
+ |
|
| 23 |
+# create required tmp and log directories |
|
| 24 |
+sudo -u huginn -H mkdir -p tmp/pids tmp/cache tmp/sockets log |
|
| 25 |
+chmod -R u+rwX log tmp |
|
| 26 |
+ |
|
| 27 |
+# install gems required by Huginn, use local cache if available |
|
| 28 |
+if [ -d "/scripts/cache" ]; then |
|
| 29 |
+ mv /scripts/cache vendor/ |
|
| 30 |
+ chown -R huginn:huginn vendor/cache |
|
| 31 |
+fi |
|
| 32 |
+sudo -u huginn -H bundle install --deployment --without development test |
|
| 33 |
+ |
|
| 34 |
+# silence setlocale message (THANKS DEBIAN!) |
|
| 35 |
+cat > /etc/default/locale <<EOF |
|
| 36 |
+LC_ALL=en_US.UTF-8 |
|
| 37 |
+LANG=en_US.UTF-8 |
|
| 38 |
+EOF |
|
| 39 |
+ |
@@ -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 |
@@ -0,0 +1,110 @@ |
||
| 1 |
+require 'liquid' |
|
| 2 |
+ |
|
| 3 |
+Location = Struct.new(:lat, :lng, :radius, :speed, :course) |
|
| 4 |
+ |
|
| 5 |
+class Location |
|
| 6 |
+ include LiquidDroppable |
|
| 7 |
+ |
|
| 8 |
+ protected :[]= |
|
| 9 |
+ |
|
| 10 |
+ def initialize(data = {})
|
|
| 11 |
+ super() |
|
| 12 |
+ |
|
| 13 |
+ case data |
|
| 14 |
+ when Array |
|
| 15 |
+ raise ArgumentError, 'unsupported location data' unless data.size == 2 |
|
| 16 |
+ self.lat, self.lng = data |
|
| 17 |
+ when Hash, Location |
|
| 18 |
+ data.each { |key, value|
|
|
| 19 |
+ case key.to_sym |
|
| 20 |
+ when :lat, :latitude |
|
| 21 |
+ self.lat = value |
|
| 22 |
+ when :lng, :longitude |
|
| 23 |
+ self.lng = value |
|
| 24 |
+ when :radius |
|
| 25 |
+ self.radius = value |
|
| 26 |
+ when :speed |
|
| 27 |
+ self.speed = value |
|
| 28 |
+ when :course |
|
| 29 |
+ self.course = value |
|
| 30 |
+ end |
|
| 31 |
+ } |
|
| 32 |
+ else |
|
| 33 |
+ raise ArgumentError, 'unsupported location data' |
|
| 34 |
+ end |
|
| 35 |
+ |
|
| 36 |
+ yield self if block_given? |
|
| 37 |
+ end |
|
| 38 |
+ |
|
| 39 |
+ def lat=(value) |
|
| 40 |
+ self[:lat] = floatify(value) { |f|
|
|
| 41 |
+ if f.abs <= 90 |
|
| 42 |
+ f |
|
| 43 |
+ else |
|
| 44 |
+ raise ArgumentError, 'out of bounds' |
|
| 45 |
+ end |
|
| 46 |
+ } |
|
| 47 |
+ end |
|
| 48 |
+ |
|
| 49 |
+ alias latitude lat |
|
| 50 |
+ alias latitude= lat= |
|
| 51 |
+ |
|
| 52 |
+ def lng=(value) |
|
| 53 |
+ self[:lng] = floatify(value) { |f|
|
|
| 54 |
+ if f.abs <= 180 |
|
| 55 |
+ f |
|
| 56 |
+ else |
|
| 57 |
+ raise ArgumentError, 'out of bounds' |
|
| 58 |
+ end |
|
| 59 |
+ } |
|
| 60 |
+ end |
|
| 61 |
+ |
|
| 62 |
+ alias longitude lng |
|
| 63 |
+ alias longitude= lng= |
|
| 64 |
+ |
|
| 65 |
+ def radius=(value) |
|
| 66 |
+ self[:radius] = floatify(value) { |f| f if f >= 0 }
|
|
| 67 |
+ end |
|
| 68 |
+ |
|
| 69 |
+ def speed=(value) |
|
| 70 |
+ self[:speed] = floatify(value) { |f| f if f >= 0 }
|
|
| 71 |
+ end |
|
| 72 |
+ |
|
| 73 |
+ def course=(value) |
|
| 74 |
+ self[:course] = floatify(value) { |f| f if (0..360).cover?(f) }
|
|
| 75 |
+ end |
|
| 76 |
+ |
|
| 77 |
+ def present? |
|
| 78 |
+ lat && lng |
|
| 79 |
+ end |
|
| 80 |
+ |
|
| 81 |
+ def empty? |
|
| 82 |
+ !present? |
|
| 83 |
+ end |
|
| 84 |
+ |
|
| 85 |
+ private |
|
| 86 |
+ |
|
| 87 |
+ def floatify(value) |
|
| 88 |
+ case value |
|
| 89 |
+ when nil, '' |
|
| 90 |
+ return nil |
|
| 91 |
+ else |
|
| 92 |
+ float = Float(value) |
|
| 93 |
+ if block_given? |
|
| 94 |
+ yield(float) |
|
| 95 |
+ else |
|
| 96 |
+ float |
|
| 97 |
+ end |
|
| 98 |
+ end |
|
| 99 |
+ end |
|
| 100 |
+end |
|
| 101 |
+ |
|
| 102 |
+class LocationDrop |
|
| 103 |
+ KEYS = Location.members.map(&:to_s).concat(%w[latitude longitude]) |
|
| 104 |
+ |
|
| 105 |
+ def before_method(key) |
|
| 106 |
+ if KEYS.include?(key) |
|
| 107 |
+ @object.__send__(key) |
|
| 108 |
+ end |
|
| 109 |
+ end |
|
| 110 |
+end |
@@ -1,6 +1,5 @@ |
||
| 1 | 1 |
require 'cgi' |
| 2 | 2 |
require 'json' |
| 3 |
-require 'twitter/json_stream' |
|
| 4 | 3 |
require 'em-http-request' |
| 5 | 4 |
require 'pp' |
| 6 | 5 |
|
@@ -88,6 +87,14 @@ class TwitterStream |
||
| 88 | 87 |
SEPARATOR = /[^\w_\-]+/ |
| 89 | 88 |
|
| 90 | 89 |
def run |
| 90 |
+ if Agents::TwitterStreamAgent.dependencies_missing? |
|
| 91 |
+ STDERR.puts Agents::TwitterStreamAgent.twitter_dependencies_missing |
|
| 92 |
+ STDERR.flush |
|
| 93 |
+ return |
|
| 94 |
+ end |
|
| 95 |
+ |
|
| 96 |
+ require 'twitter/json_stream' |
|
| 97 |
+ |
|
| 91 | 98 |
while @running |
| 92 | 99 |
begin |
| 93 | 100 |
agents = Agents::TwitterStreamAgent.active.all |
@@ -0,0 +1,68 @@ |
||
| 1 |
+require 'spec_helper' |
|
| 2 |
+ |
|
| 3 |
+describe Location do |
|
| 4 |
+ let(:location) {
|
|
| 5 |
+ Location.new( |
|
| 6 |
+ lat: BigDecimal.new('2.0'),
|
|
| 7 |
+ lng: BigDecimal.new('3.0'),
|
|
| 8 |
+ radius: 300, |
|
| 9 |
+ speed: 2, |
|
| 10 |
+ course: 30) |
|
| 11 |
+ } |
|
| 12 |
+ |
|
| 13 |
+ it "converts values to Float" do |
|
| 14 |
+ expect(location.lat).to be_a Float |
|
| 15 |
+ expect(location.lat).to eq 2.0 |
|
| 16 |
+ expect(location.lng).to be_a Float |
|
| 17 |
+ expect(location.lng).to eq 3.0 |
|
| 18 |
+ expect(location.radius).to be_a Float |
|
| 19 |
+ expect(location.radius).to eq 300.0 |
|
| 20 |
+ expect(location.speed).to be_a Float |
|
| 21 |
+ expect(location.speed).to eq 2.0 |
|
| 22 |
+ expect(location.course).to be_a Float |
|
| 23 |
+ expect(location.course).to eq 30.0 |
|
| 24 |
+ end |
|
| 25 |
+ |
|
| 26 |
+ it "provides hash-style access to its properties with both symbol and string keys" do |
|
| 27 |
+ expect(location[:lat]).to be_a Float |
|
| 28 |
+ expect(location[:lat]).to eq 2.0 |
|
| 29 |
+ expect(location['lat']).to be_a Float |
|
| 30 |
+ expect(location['lat']).to eq 2.0 |
|
| 31 |
+ end |
|
| 32 |
+ |
|
| 33 |
+ it "does not allow hash-style assignment" do |
|
| 34 |
+ expect {
|
|
| 35 |
+ location[:lat] = 2.0 |
|
| 36 |
+ }.to raise_error |
|
| 37 |
+ end |
|
| 38 |
+ |
|
| 39 |
+ it "ignores invalid values" do |
|
| 40 |
+ location2 = Location.new( |
|
| 41 |
+ lat: 2, |
|
| 42 |
+ lng: 3, |
|
| 43 |
+ radius: -1, |
|
| 44 |
+ speed: -1, |
|
| 45 |
+ course: -1) |
|
| 46 |
+ expect(location2.radius).to be_nil |
|
| 47 |
+ expect(location2.speed).to be_nil |
|
| 48 |
+ expect(location2.course).to be_nil |
|
| 49 |
+ end |
|
| 50 |
+ |
|
| 51 |
+ it "considers a location empty if either latitude or longitude is missing" do |
|
| 52 |
+ expect(Location.new.empty?).to be_truthy |
|
| 53 |
+ expect(Location.new(lat: 2, radius: 1).present?).to be_falsy |
|
| 54 |
+ expect(Location.new(lng: 3, radius: 1).present?).to be_falsy |
|
| 55 |
+ end |
|
| 56 |
+ |
|
| 57 |
+ it "is droppable" do |
|
| 58 |
+ {
|
|
| 59 |
+ '{{location.lat}}' => '2.0',
|
|
| 60 |
+ '{{location.latitude}}' => '2.0',
|
|
| 61 |
+ '{{location.lng}}' => '3.0',
|
|
| 62 |
+ '{{location.longitude}}' => '3.0',
|
|
| 63 |
+ }.each { |template, result|
|
|
| 64 |
+ expect(Liquid::Template.parse(template).render('location' => location.to_liquid)).to eq(result),
|
|
| 65 |
+ "expected #{template.inspect} to expand to #{result.inspect}"
|
|
| 66 |
+ } |
|
| 67 |
+ end |
|
| 68 |
+end |
@@ -1,6 +1,50 @@ |
||
| 1 | 1 |
require 'spec_helper' |
| 2 | 2 |
|
| 3 | 3 |
describe Event do |
| 4 |
+ describe ".with_location" do |
|
| 5 |
+ it "selects events with location" do |
|
| 6 |
+ event = events(:bob_website_agent_event) |
|
| 7 |
+ event.lat = 2 |
|
| 8 |
+ event.lng = 3 |
|
| 9 |
+ event.save! |
|
| 10 |
+ Event.with_location.pluck(:id).should == [event.id] |
|
| 11 |
+ |
|
| 12 |
+ event.lat = nil |
|
| 13 |
+ event.save! |
|
| 14 |
+ Event.with_location.should be_empty |
|
| 15 |
+ end |
|
| 16 |
+ end |
|
| 17 |
+ |
|
| 18 |
+ describe "#location" do |
|
| 19 |
+ it "returns a default hash when an event does not have a location" do |
|
| 20 |
+ event = events(:bob_website_agent_event) |
|
| 21 |
+ event.location.should == Location.new( |
|
| 22 |
+ lat: nil, |
|
| 23 |
+ lng: nil, |
|
| 24 |
+ radius: 0.0, |
|
| 25 |
+ speed: nil, |
|
| 26 |
+ course: nil) |
|
| 27 |
+ end |
|
| 28 |
+ |
|
| 29 |
+ it "returns a hash containing location information" do |
|
| 30 |
+ event = events(:bob_website_agent_event) |
|
| 31 |
+ event.lat = 2 |
|
| 32 |
+ event.lng = 3 |
|
| 33 |
+ event.payload = {
|
|
| 34 |
+ radius: 300, |
|
| 35 |
+ speed: 0.5, |
|
| 36 |
+ course: 90.0, |
|
| 37 |
+ } |
|
| 38 |
+ event.save! |
|
| 39 |
+ event.location.should == Location.new( |
|
| 40 |
+ lat: 2.0, |
|
| 41 |
+ lng: 3.0, |
|
| 42 |
+ radius: 0.0, |
|
| 43 |
+ speed: 0.5, |
|
| 44 |
+ course: 90.0) |
|
| 45 |
+ end |
|
| 46 |
+ end |
|
| 47 |
+ |
|
| 4 | 48 |
describe "#reemit" do |
| 5 | 49 |
it "creates a new event identical to itself" do |
| 6 | 50 |
events(:bob_website_agent_event).lat = 2 |
@@ -130,6 +174,8 @@ describe EventDrop do |
||
| 130 | 174 |
'title' => 'some title', |
| 131 | 175 |
'url' => 'http://some.site.example.org/', |
| 132 | 176 |
} |
| 177 |
+ @event.lat = 2 |
|
| 178 |
+ @event.lng = 3 |
|
| 133 | 179 |
@event.save! |
| 134 | 180 |
end |
| 135 | 181 |
|
@@ -166,4 +212,9 @@ describe EventDrop do |
||
| 166 | 212 |
t = '{{created_at | date:"%FT%T%z" }}'
|
| 167 | 213 |
interpolate(t, @event).should eq(@event.created_at.strftime("%FT%T%z"))
|
| 168 | 214 |
end |
| 215 |
+ |
|
| 216 |
+ it 'should have _location_' do |
|
| 217 |
+ t = '{{_location_.lat}},{{_location_.lng}}'
|
|
| 218 |
+ interpolate(t, @event).should eq("2.0,3.0")
|
|
| 219 |
+ end |
|
| 169 | 220 |
end |
@@ -1,3 +1,4 @@ |
||
| 1 |
+ |
|
| 1 | 2 |
/* |
| 2 | 3 |
Copyright (c) 2014, Andrew Cantino |
| 3 | 4 |
Copyright (c) 2009, Andrew Cantino & Kyle Maxwell |
@@ -528,7 +529,7 @@ |
||
| 528 | 529 |
} |
| 529 | 530 |
innerbq.append($('<span class="colon">: </span>'));
|
| 530 | 531 |
newElem = this.build(jsonvalue, innerbq, json, jsonkey, root); |
| 531 |
- if (newElem && newElem.text() === "??") {
|
|
| 532 |
+ if (!elem && newElem && newElem.text() === "??") {
|
|
| 532 | 533 |
elem = newElem; |
| 533 | 534 |
} |
| 534 | 535 |
bq.append(innerbq); |