merged master

Matthew Werner 11 lat temu
rodzic
commit
a70a6943ae
78 zmienionych plików z 3077 dodań i 747 usunięć
  1. 35 7
      .env.example
  2. 1 0
      .gitignore
  3. 5 1
      .travis.yml
  4. 2 1
      CHANGES.md
  5. 69 50
      Gemfile
  6. 191 182
      Gemfile.lock
  7. 1 1
      Procfile
  8. 9 24
      README.md
  9. 3 3
      app/assets/javascripts/application.js.coffee.erb
  10. 39 0
      app/concerns/json_path_options_overwritable.rb
  11. 15 0
      app/concerns/working_helpers.rb
  12. 9 1
      app/controllers/agents_controller.rb
  13. 40 0
      app/helpers/dot_helper.rb
  14. 37 27
      app/models/agent.rb
  15. 90 0
      app/models/agents/basecamp_agent.rb
  16. 119 3
      app/models/agents/event_formatting_agent.rb
  17. 62 0
      app/models/agents/hipchat_agent.rb
  18. 68 10
      app/models/agents/post_agent.rb
  19. 67 0
      app/models/agents/pushbullet_agent.rb
  20. 107 0
      app/models/agents/pushover_agent.rb
  21. 111 0
      app/models/agents/shell_command_agent.rb
  22. 100 0
      app/models/agents/stubhub_agent.rb
  23. 36 12
      app/models/agents/trigger_agent.rb
  24. 10 10
      app/models/agents/twilio_agent.rb
  25. 164 74
      app/models/agents/website_agent.rb
  26. 2 2
      app/models/user.rb
  27. 1 12
      app/views/agents/diagram.html.erb
  28. 1 0
      app/views/agents/show.html.erb
  29. 1 1
      app/views/layouts/_messages.html.erb
  30. 3 0
      bin/bundle
  31. 4 0
      bin/rails
  32. 4 0
      bin/rake
  33. 1 1
      bin/schedule.rb
  34. 1 13
      config/application.rb
  35. 7 8
      config/environments/development.rb
  36. 29 13
      config/environments/production.rb
  37. 0 76
      config/environments/staging.rb
  38. 6 4
      config/environments/test.rb
  39. 8 5
      config/initializers/devise.rb
  40. 1 1
      config/initializers/secret_token.rb
  41. 4 4
      config/routes.rb
  42. 1 1
      db/seeds.rb
  43. 3 0
      deployment/.chef/knife.rb
  44. 71 0
      deployment/Cheffile.lock
  45. 28 27
      deployment/Vagrantfile
  46. 1 0
      deployment/roles/huginn_development.json
  47. 1 1
      deployment/roles/huginn_production.json
  48. 13 6
      deployment/site-cookbooks/huginn_development/recipes/default.rb
  49. 0 58
      deployment/site-cookbooks/huginn_production/files/default/Gemfile
  50. 4 4
      deployment/site-cookbooks/huginn_production/files/default/Procfile
  51. 68 7
      deployment/site-cookbooks/huginn_production/files/default/env.example
  52. 5 6
      deployment/site-cookbooks/huginn_production/files/default/nginx.conf
  53. 4 2
      deployment/site-cookbooks/huginn_production/files/default/unicorn.rb
  54. 18 9
      deployment/site-cookbooks/huginn_production/recipes/default.rb
  55. 0 6
      deployment/solo.rb
  56. 2 2
      lib/rdbms_functions.rb
  57. 2 2
      lib/utils.rb
  58. 16 0
      spec/controllers/agents_controller_spec.rb
  59. 21 0
      spec/data_fixtures/basecamp.json
  60. 17 0
      spec/data_fixtures/stubhub_data.json
  61. 48 0
      spec/helpers/dot_helper_spec.rb
  62. 1 1
      spec/lib/utils_spec.rb
  63. 48 26
      spec/models/agent_spec.rb
  64. 85 0
      spec/models/agents/basecamp_agent_spec.rb
  65. 41 4
      spec/models/agents/event_formatting_agent_spec.rb
  66. 83 0
      spec/models/agents/hipchat_agent_spec.rb
  67. 127 14
      spec/models/agents/post_agent_spec.rb
  68. 19 14
      spec/models/agents/public_transport_agent_spec.rb
  69. 80 0
      spec/models/agents/pushbullet_agent_spec.rb
  70. 222 0
      spec/models/agents/pushover_agent_spec.rb
  71. 99 0
      spec/models/agents/shell_command_agent_spec.rb
  72. 67 0
      spec/models/agents/stubhub_agent_spec.rb
  73. 143 1
      spec/models/agents/trigger_agent_spec.rb
  74. 184 8
      spec/models/agents/website_agent_spec.rb
  75. 31 0
      spec/models/concerns/json_path_options_overwritable.rb
  76. 53 0
      spec/models/concerns/working_helpers.rb
  77. 8 2
      spec/spec_helper.rb
  78. 0 0
      tmp/.gitkeep

+ 35 - 7
.env.example

@@ -31,6 +31,17 @@ DATABASE_PASSWORD=""
31 31
 # Configure Rails environment.  This should only be needed in production and may cause errors in development.
32 32
 # RAILS_ENV=production
33 33
 
34
+# Should Rails force all requests to use SSL?
35
+FORCE_SSL=false
36
+
37
+############################
38
+#     Allowing Signups     #
39
+############################
40
+
41
+# This invitation code will be required for users to signup with your Huginn installation.
42
+# You can see its use in user.rb.  PLEASE CHANGE THIS!
43
+INVITATION_CODE=try-huginn
44
+
34 45
 #############################
35 46
 #    Email Configuration    #
36 47
 #############################
@@ -52,13 +63,6 @@ SMTP_ENABLE_STARTTLS_AUTO=true
52 63
 # The address from which system emails will appear to be sent.
53 64
 EMAIL_FROM_ADDRESS=from_address@gmail.com
54 65
 
55
-############################
56
-#     Allowing Signups     #
57
-############################
58
-
59
-# This invitation code will be required for users to signup with your Huginn installation.
60
-# You can see its use in user.rb.
61
-INVITATION_CODE=try-huginn
62 66
 
63 67
 ###########################
64 68
 #      Agent Logging      #
@@ -77,3 +81,27 @@ AWS_ACCESS_KEY="your aws access key"
77 81
 
78 82
 # Set AWS_SANDBOX to true if you're developing Huginn code.
79 83
 AWS_SANDBOX=false
84
+
85
+########################
86
+#   Various Settings   #
87
+########################
88
+
89
+# Specify the HTTP backend library for Faraday, used in WebsiteAgent.
90
+# You can change this depending on the performance and stability you
91
+# need for your service.  Any choice other than "typhoeus",
92
+# "net_http", or "em_http" should require you to bundle a corresponding
93
+# gem via Gemfile.
94
+FARADAY_HTTP_BACKEND=typhoeus
95
+
96
+# Allow JSONPath eval expresions. i.e., $..price[?(@ < 20)]
97
+# You should not allow this on a shared Huginn box because it is not secure.
98
+ALLOW_JSONPATH_EVAL=false
99
+
100
+# Enable this setting to allow insecure Agents like the ShellCommandAgent.  Only do this
101
+# when you trust everyone using your Huginn installation.
102
+ENABLE_INSECURE_AGENTS=false
103
+
104
+# Use Graphviz for generating diagrams instead of using Google Chart
105
+# Tools.  Specify a dot(1) command path built with SVG support
106
+# enabled.
107
+#USE_GRAPHVIZ_DOT=dot

+ 1 - 0
.gitignore

@@ -7,6 +7,7 @@ capybara-*.html
7 7
 /vendor/bundle
8 8
 /log/*
9 9
 /tmp/*
10
+!/tmp/.gitkeep
10 11
 /db/*.sqlite3
11 12
 /public/system/*
12 13
 /coverage/

+ 5 - 1
.travis.yml

@@ -1,9 +1,13 @@
1 1
 language: ruby
2 2
 bundler_args: --without development production
3
+env:
4
+  - APP_SECRET_TOKEN=b2724973fd81c2f4ac0f92ac48eb3f0152c4a11824c122bcf783419a4c51d8b9bba81c8ba6a66c7de599677c7f486242cf819775c433908e77c739c5c8ae118d
3 5
 rvm:
4 6
   - 2.0.0
5 7
   - 2.1.1
6 8
   - 1.9.3
9
+before_install:
10
+  - travis_retry gem install bundler  
7 11
 before_script:
8 12
   - mysql -e 'create database huginn_test;'
9 13
   - bundle exec rake db:migrate db:test:prepare
@@ -13,6 +17,6 @@ notifications:
13 17
     channels:
14 18
       - "chat.freenode.net#huginn"
15 19
     template:
16
-      - "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}"
20
+      - "<%{author}> %{branch} - %{commit} (%{commit_message}): %{message}"
17 21
       - "Change view : %{compare_url}"
18 22
       - "Build details : %{build_url}"

+ 2 - 1
CHANGES.md

@@ -1,6 +1,7 @@
1 1
 # Changes
2 2
 
3
-* 0.4 (April 10, 2014) - WebHooksController has been renamed to WebRequestsController and all HTTP verbs are now accepted and passed through to Agents' #receive_web_request method. The new DataOutputAgent returns JSON or RSS feeds of incoming Events via external web request.
3
+* 0.5 (April 20, 2014) - Tons of new additions! FtpsiteAgent; WebsiteAgent has xpath, multiple URL, and encoding support; regexp extractions in EventFormattingAgent; PostAgent takes default params and headers, and can make GET requests; local Graphviz support; ShellCommandAgent; BasecampAgent; HipchatAgent; and lots of bug fixes!
4
+* 0.4 (April 10, 2014) - WebHooksController has been renamed to WebRequestsController and all HTTP verbs are now accepted and passed through to Agents' #receive\_web\_request method. The new DataOutputAgent returns JSON or RSS feeds of incoming Events via external web request.  [Documentation is on the wiki.](https://github.com/cantino/huginn/wiki/Creating-a-new-agent#receiving-web-requests).
4 5
 * 0.31 (Jan 2, 2014)   - Agents now have an optional keep\_events\_for option that is propagated to created events' expires\_at field, and they update their events' expires\_at fields on change.
5 6
 * 0.3 (Jan 1, 2014)    - Remove symbolization of memory, options, and payloads; convert memory, options, and payloads to JSON from YAML.  Migration will perform conversion and adjust tables to be UTF-8.  Recommend making a DB backup before migrating.
6 7
 * 0.2 (Nov 6, 2013)    - PeakDetectorAgent now uses `window_duration_in_days` and `min_peak_spacing_in_days`.  Additionally, peaks trigger when the time series rises over the standard deviation multiple, not after it starts to fall.

+ 69 - 50
Gemfile

@@ -1,67 +1,86 @@
1 1
 source 'https://rubygems.org'
2 2
 
3
-gem 'rails'
4
-gem 'rake'
5
-gem 'mysql2'
6
-gem 'devise'
7
-gem 'kaminari'
8
-gem 'bootstrap-kaminari-views'
9
-gem "rufus-scheduler", :require => false
10
-gem 'json', '>= 1.7.7'
11
-gem 'jsonpath'
12
-gem 'twilio-ruby'
13
-gem 'ruby-growl'
14
-
15
-gem 'delayed_job'
16
-gem 'delayed_job_active_record'#, "~> 0.3.3" # newer was giving a strange MySQL error
17
-gem "daemons"
3
+gem 'protected_attributes', '~>1.0.7'
18 4
 
19
-# To enable DelayedJobWeb, see the 'Enable DelayedJobWeb' section of the README.
20
-# gem "delayed_job_web"
21
-
22
-gem 'foreman'
23
-gem 'dotenv-rails', :groups => [:development, :test]
24
-
25
-gem 'sass-rails',   '~> 3.2.3'
26
-gem 'coffee-rails', '~> 3.2.1'
27
-gem 'uglifier', '>= 1.0.3'
28
-gem 'select2-rails'
29
-gem 'jquery-rails'
30
-gem 'ace-rails-ap'
31
-
32
-gem 'geokit-rails3'
33
-gem 'kramdown'
34
-gem "typhoeus"
35
-gem 'nokogiri'
36
-gem 'wunderground'
37
-gem 'forecast_io'
38
-gem 'rturk'
39
-
40
-gem "twitter", '~> 5.7.1'
41
-gem 'twitter-stream', :git => 'https://github.com/cantino/twitter-stream', :branch => 'master'
42
-gem 'em-http-request'
43
-gem 'weibo_2'
44
-
45
-gem 'xmpp4r',   '~> 0.5.6'
46
-
47
-gem 'therubyracer'
48
-
49
-platforms :ruby_18 do
50
-  gem 'system_timer'
51
-  gem 'fastercsv'
5
+gem 'rails', '4.1.0'
6
+
7
+case RUBY_PLATFORM
8
+when /freebsd/i
9
+  # Seems FreeBSD's zoneinfo is not exactly what tzinfo expects
10
+  gem 'tzinfo-data'
11
+else
12
+  # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
13
+  gem 'tzinfo-data', platforms: [:mswin]
52 14
 end
53 15
 
16
+gem 'mysql2', '~> 0.3.15'
17
+gem 'devise', '~> 3.2.4'
18
+gem 'kaminari', '~> 0.15.1'
19
+gem 'bootstrap-kaminari-views', '~> 0.0.2'
20
+gem 'rufus-scheduler', '~> 3.0.7', require: false
21
+gem 'json', '~> 1.8.1'
22
+gem 'jsonpath', '~> 0.5.3'
23
+gem 'twilio-ruby', '~> 3.11.5'
24
+gem 'ruby-growl', '~> 4.1.0'
25
+
26
+gem 'delayed_job', '~> 4.0.0'
27
+gem 'delayed_job_active_record', '~> 4.0.0'
28
+gem 'daemons', '~> 1.1.9'
29
+
30
+# To enable DelayedJobWeb, see the 'Enable DelayedJobWeb' section of the README.
31
+# gem 'delayed_job_web'
32
+
33
+gem 'foreman', '~> 0.63.0'
34
+
35
+gem 'sass-rails',   '~> 4.0.0'
36
+gem 'coffee-rails', '~> 4.0.0'
37
+gem 'uglifier', '>= 1.3.0'
38
+gem 'select2-rails', '~> 3.5.4'
39
+gem 'jquery-rails', '~> 3.1.0'
40
+gem 'ace-rails-ap', '~> 2.0.1'
41
+
42
+# geokit-rails doesn't work with geokit 1.8.X but it specifies ~> 1.5
43
+# in its own Gemfile.
44
+gem 'geokit', '~> 1.8.4'
45
+gem 'geokit-rails', '~> 2.0.1'
46
+
47
+gem 'kramdown', '~> 1.3.3'
48
+gem 'faraday', '~> 0.9.0'
49
+gem 'faraday_middleware'
50
+gem 'typhoeus', '~> 0.6.3'
51
+gem 'nokogiri', '~> 1.6.1'
52
+
53
+gem 'wunderground', '~> 1.2.0'
54
+gem 'forecast_io', '~> 2.0.0'
55
+gem 'rturk', '~> 2.12.1'
56
+
57
+gem 'twitter', '~> 5.8.0'
58
+gem 'twitter-stream', github: 'cantino/twitter-stream', branch: 'master'
59
+gem 'em-http-request', '~> 1.1.2'
60
+gem 'weibo_2', '~> 0.1.4'
61
+gem 'hipchat', '~> 1.1.0'
62
+gem 'xmpp4r',  '~> 0.5.6'
63
+
64
+gem 'therubyracer', '~> 0.12.1'
65
+
54 66
 group :development do
55 67
   gem 'binding_of_caller'
56 68
   gem 'better_errors'
57 69
 end
58 70
 
59 71
 group :development, :test do
72
+  gem 'dotenv-rails'
60 73
   gem 'pry'
61 74
   gem 'rspec-rails'
62 75
   gem 'rspec'
63 76
   gem 'shoulda-matchers'
64 77
   gem 'rr'
65
-  gem 'webmock', :require => false
66
-  gem 'coveralls', :require => false
78
+  gem 'delorean'
79
+  gem 'webmock', require: false
80
+  gem 'coveralls', require: false
81
+end
82
+
83
+group :production do
84
+  gem 'dotenv-deployment'
85
+  gem 'rack'
67 86
 end

+ 191 - 182
Gemfile.lock

@@ -1,5 +1,5 @@
1 1
 GIT
2
-  remote: https://github.com/cantino/twitter-stream
2
+  remote: git://github.com/cantino/twitter-stream.git
3 3
   revision: fde6bed2b62ca487d49e4a57381bbfca6e33361b
4 4
   branch: master
5 5
   specs:
@@ -12,81 +12,85 @@ GEM
12 12
   remote: https://rubygems.org/
13 13
   specs:
14 14
     ace-rails-ap (2.0.1)
15
-    actionmailer (3.2.13)
16
-      actionpack (= 3.2.13)
17
-      mail (~> 2.5.3)
18
-    actionpack (3.2.13)
19
-      activemodel (= 3.2.13)
20
-      activesupport (= 3.2.13)
21
-      builder (~> 3.0.0)
15
+    actionmailer (4.1.0)
16
+      actionpack (= 4.1.0)
17
+      actionview (= 4.1.0)
18
+      mail (~> 2.5.4)
19
+    actionpack (4.1.0)
20
+      actionview (= 4.1.0)
21
+      activesupport (= 4.1.0)
22
+      rack (~> 1.5.2)
23
+      rack-test (~> 0.6.2)
24
+    actionview (4.1.0)
25
+      activesupport (= 4.1.0)
26
+      builder (~> 3.1)
22 27
       erubis (~> 2.7.0)
23
-      journey (~> 1.0.4)
24
-      rack (~> 1.4.5)
25
-      rack-cache (~> 1.2)
26
-      rack-test (~> 0.6.1)
27
-      sprockets (~> 2.2.1)
28
-    activemodel (3.2.13)
29
-      activesupport (= 3.2.13)
30
-      builder (~> 3.0.0)
31
-    activerecord (3.2.13)
32
-      activemodel (= 3.2.13)
33
-      activesupport (= 3.2.13)
34
-      arel (~> 3.0.2)
35
-      tzinfo (~> 0.3.29)
36
-    activeresource (3.2.13)
37
-      activemodel (= 3.2.13)
38
-      activesupport (= 3.2.13)
39
-    activesupport (3.2.13)
40
-      i18n (= 0.6.1)
41
-      multi_json (~> 1.0)
42
-    addressable (2.3.5)
43
-    arel (3.0.3)
44
-    atomic (1.1.14)
45
-    bcrypt-ruby (3.1.1)
28
+    activemodel (4.1.0)
29
+      activesupport (= 4.1.0)
30
+      builder (~> 3.1)
31
+    activerecord (4.1.0)
32
+      activemodel (= 4.1.0)
33
+      activesupport (= 4.1.0)
34
+      arel (~> 5.0.0)
35
+    activesupport (4.1.0)
36
+      i18n (~> 0.6, >= 0.6.9)
37
+      json (~> 1.7, >= 1.7.7)
38
+      minitest (~> 5.1)
39
+      thread_safe (~> 0.1)
40
+      tzinfo (~> 1.1)
41
+    addressable (2.3.6)
42
+    arel (5.0.1.20140414130214)
43
+    bcrypt (3.1.7)
46 44
     better_errors (1.1.0)
47 45
       coderay (>= 1.0.0)
48 46
       erubis (>= 2.6.6)
49 47
     binding_of_caller (0.7.2)
50 48
       debug_inspector (>= 0.0.1)
51
-    bootstrap-kaminari-views (0.0.2)
49
+    bootstrap-kaminari-views (0.0.3)
52 50
       kaminari (>= 0.13)
53 51
       rails (>= 3.1)
54 52
     buftok (0.2.0)
55
-    builder (3.0.4)
56
-    coderay (1.0.9)
57
-    coffee-rails (3.2.2)
53
+    builder (3.2.2)
54
+    chronic (0.10.2)
55
+    coderay (1.1.0)
56
+    coffee-rails (4.0.1)
58 57
       coffee-script (>= 2.2.0)
59
-      railties (~> 3.2.0)
58
+      railties (>= 4.0.0, < 5.0)
60 59
     coffee-script (2.2.0)
61 60
       coffee-script-source
62 61
       execjs
63
-    coffee-script-source (1.6.3)
64
-    cookiejar (0.3.1)
62
+    coffee-script-source (1.7.0)
63
+    cookiejar (0.3.2)
65 64
     coveralls (0.7.0)
66 65
       multi_json (~> 1.3)
67 66
       rest-client
68 67
       simplecov (>= 0.7)
69 68
       term-ansicolor
70 69
       thor
71
-    crack (0.4.1)
72
-      safe_yaml (~> 0.9.0)
70
+    crack (0.4.2)
71
+      safe_yaml (~> 1.0.0)
73 72
     daemons (1.1.9)
74 73
     debug_inspector (0.0.2)
75
-    delayed_job (4.0.0)
76
-      activesupport (>= 3.0, < 4.1)
77
-    delayed_job_active_record (4.0.0)
78
-      activerecord (>= 3.0, < 4.1)
74
+    delayed_job (4.0.1)
75
+      activesupport (>= 3.0, < 4.2)
76
+    delayed_job_active_record (4.0.1)
77
+      activerecord (>= 3.0, < 4.2)
79 78
       delayed_job (>= 3.0, < 4.1)
80
-    devise (3.0.0)
81
-      bcrypt-ruby (~> 3.0)
79
+    delorean (2.1.0)
80
+      chronic
81
+    devise (3.2.4)
82
+      bcrypt (~> 3.0)
82 83
       orm_adapter (~> 0.1)
83 84
       railties (>= 3.2.6, < 5)
85
+      thread_safe (~> 0.1)
84 86
       warden (~> 1.2.3)
85
-    diff-lcs (1.2.4)
86
-    docile (1.1.1)
87
-    dotenv (0.9.0)
88
-    dotenv-rails (0.9.0)
89
-      dotenv (= 0.9.0)
87
+    diff-lcs (1.2.5)
88
+    docile (1.1.3)
89
+    dotenv (0.11.1)
90
+      dotenv-deployment (~> 0.0.2)
91
+    dotenv-deployment (0.0.2)
92
+    dotenv-rails (0.11.1)
93
+      dotenv (= 0.11.1)
90 94
     em-http-request (1.1.2)
91 95
       addressable (>= 2.3.4)
92 96
       cookiejar
@@ -96,19 +100,18 @@ GEM
96 100
     em-socksify (0.3.0)
97 101
       eventmachine (>= 1.0.0.beta.4)
98 102
     equalizer (0.0.9)
99
-    erector (0.9.0)
103
+    erector (0.10.0)
100 104
       treetop (>= 1.2.3)
101 105
     erubis (2.7.0)
102
-    ethon (0.5.12)
106
+    ethon (0.7.0)
103 107
       ffi (>= 1.3.0)
104
-      mime-types (~> 1.18)
105 108
     eventmachine (1.0.3)
106
-    execjs (1.4.0)
107
-      multi_json (~> 1.0)
109
+    execjs (2.0.2)
108 110
     faraday (0.9.0)
109 111
       multipart-post (>= 1.2, < 3)
110
-    fastercsv (1.5.5)
111
-    ffi (1.9.0)
112
+    faraday_middleware (0.9.1)
113
+      faraday (>= 0.7.4, < 0.10)
114
+    ffi (1.9.3)
112 115
     forecast_io (2.0.0)
113 116
       faraday
114 117
       hashie
@@ -116,90 +119,86 @@ GEM
116 119
     foreman (0.63.0)
117 120
       dotenv (>= 0.7)
118 121
       thor (>= 0.13.6)
119
-    geokit (1.6.5)
120
-      multi_json
121
-    geokit-rails3 (0.1.5)
122
+    geokit (1.8.4)
123
+      multi_json (>= 1.3.2)
124
+    geokit-rails (2.0.1)
122 125
       geokit (~> 1.5)
123
-      rails (~> 3.0)
126
+      rails (>= 3.0)
124 127
     hashie (2.0.5)
125 128
     hike (1.2.3)
129
+    hipchat (1.1.0)
130
+      httparty
126 131
     http (0.5.0)
127 132
       http_parser.rb
128 133
     http_parser.rb (0.6.0)
129
-    httparty (0.11.0)
130
-      multi_json (~> 1.0)
134
+    httparty (0.13.1)
135
+      json (~> 1.8)
131 136
       multi_xml (>= 0.5.2)
132
-    httpauth (0.2.0)
133
-    i18n (0.6.1)
134
-    journey (1.0.4)
135
-    jquery-rails (3.0.4)
137
+    i18n (0.6.9)
138
+    jquery-rails (3.1.0)
136 139
       railties (>= 3.0, < 5.0)
137 140
       thor (>= 0.14, < 2.0)
138 141
     json (1.8.1)
139
-    jsonpath (0.5.3)
142
+    jsonpath (0.5.6)
140 143
       multi_json
141
-    jwt (0.1.8)
144
+    jwt (0.1.11)
142 145
       multi_json (>= 1.5)
143
-    kaminari (0.14.1)
146
+    kaminari (0.15.1)
144 147
       actionpack (>= 3.0.0)
145 148
       activesupport (>= 3.0.0)
146
-    kramdown (1.1.0)
149
+    kramdown (1.3.3)
147 150
     libv8 (3.16.14.3)
148
-    macaddr (1.6.7)
151
+    macaddr (1.7.1)
149 152
       systemu (~> 2.6.2)
150 153
     mail (2.5.4)
151 154
       mime-types (~> 1.16)
152 155
       treetop (~> 1.4.8)
153
-    memoizable (0.4.0)
154
-      thread_safe (~> 0.1.3)
155
-    method_source (0.8.1)
156
-    mime-types (1.24)
157
-    mini_portile (0.5.1)
158
-    multi_json (1.7.9)
156
+    memoizable (0.4.2)
157
+      thread_safe (~> 0.3, >= 0.3.1)
158
+    method_source (0.8.2)
159
+    mime-types (1.25.1)
160
+    mini_portile (0.5.3)
161
+    minitest (5.3.3)
162
+    multi_json (1.9.3)
159 163
     multi_xml (0.5.5)
160 164
     multipart-post (2.0.0)
161
-    mysql2 (0.3.13)
165
+    mysql2 (0.3.15)
162 166
     naught (1.0.0)
163
-    nokogiri (1.6.0)
167
+    nokogiri (1.6.1)
164 168
       mini_portile (~> 0.5.0)
165
-    oauth2 (0.9.2)
166
-      faraday (~> 0.8)
167
-      httpauth (~> 0.2)
168
-      jwt (~> 0.1.4)
169
-      multi_json (~> 1.0)
169
+    oauth2 (0.9.3)
170
+      faraday (>= 0.8, < 0.10)
171
+      jwt (~> 0.1.8)
172
+      multi_json (~> 1.3)
170 173
       multi_xml (~> 0.5)
171 174
       rack (~> 1.2)
172
-    orm_adapter (0.4.0)
173
-    polyglot (0.3.3)
174
-    pry (0.9.12.2)
175
-      coderay (~> 1.0.5)
175
+    orm_adapter (0.5.0)
176
+    polyglot (0.3.4)
177
+    protected_attributes (1.0.7)
178
+      activemodel (>= 4.0.1, < 5.0)
179
+    pry (0.9.12.6)
180
+      coderay (~> 1.0)
176 181
       method_source (~> 0.8)
177 182
       slop (~> 3.4)
178
-    rack (1.4.5)
179
-    rack-cache (1.2)
180
-      rack (>= 0.4)
181
-    rack-ssl (1.3.3)
182
-      rack
183
+    rack (1.5.2)
183 184
     rack-test (0.6.2)
184 185
       rack (>= 1.0)
185
-    rails (3.2.13)
186
-      actionmailer (= 3.2.13)
187
-      actionpack (= 3.2.13)
188
-      activerecord (= 3.2.13)
189
-      activeresource (= 3.2.13)
190
-      activesupport (= 3.2.13)
191
-      bundler (~> 1.0)
192
-      railties (= 3.2.13)
193
-    railties (3.2.13)
194
-      actionpack (= 3.2.13)
195
-      activesupport (= 3.2.13)
196
-      rack-ssl (~> 1.3.2)
186
+    rails (4.1.0)
187
+      actionmailer (= 4.1.0)
188
+      actionpack (= 4.1.0)
189
+      actionview (= 4.1.0)
190
+      activemodel (= 4.1.0)
191
+      activerecord (= 4.1.0)
192
+      activesupport (= 4.1.0)
193
+      bundler (>= 1.3.0, < 2.0)
194
+      railties (= 4.1.0)
195
+      sprockets-rails (~> 2.0)
196
+    railties (4.1.0)
197
+      actionpack (= 4.1.0)
198
+      activesupport (= 4.1.0)
197 199
       rake (>= 0.8.7)
198
-      rdoc (~> 3.4)
199
-      thor (>= 0.14.6, < 2.0)
200
-    rake (10.1.0)
201
-    rdoc (3.12.2)
202
-      json (~> 1.4)
200
+      thor (>= 0.18.1, < 2.0)
201
+    rake (10.3.1)
203 202
     ref (1.0.5)
204 203
     rest-client (1.6.7)
205 204
       mime-types (>= 1.16)
@@ -208,35 +207,36 @@ GEM
208 207
       rspec-core (~> 2.14.0)
209 208
       rspec-expectations (~> 2.14.0)
210 209
       rspec-mocks (~> 2.14.0)
211
-    rspec-core (2.14.5)
212
-    rspec-expectations (2.14.2)
210
+    rspec-core (2.14.8)
211
+    rspec-expectations (2.14.5)
213 212
       diff-lcs (>= 1.1.3, < 2.0)
214
-    rspec-mocks (2.14.3)
215
-    rspec-rails (2.14.0)
213
+    rspec-mocks (2.14.6)
214
+    rspec-rails (2.14.2)
216 215
       actionpack (>= 3.0)
216
+      activemodel (>= 3.0)
217 217
       activesupport (>= 3.0)
218 218
       railties (>= 3.0)
219 219
       rspec-core (~> 2.14.0)
220 220
       rspec-expectations (~> 2.14.0)
221 221
       rspec-mocks (~> 2.14.0)
222
-    rturk (2.11.0)
222
+    rturk (2.12.1)
223 223
       erector
224 224
       nokogiri
225 225
       rest-client
226 226
     ruby-growl (4.1)
227 227
       uuid (~> 2.3, >= 2.3.5)
228
-    rufus-scheduler (3.0.2)
228
+    rufus-scheduler (3.0.7)
229 229
       tzinfo
230
-    safe_yaml (0.9.5)
231
-    sass (3.2.9)
232
-    sass-rails (3.2.6)
233
-      railties (~> 3.2.0)
234
-      sass (>= 3.1.10)
235
-      tilt (~> 1.3)
236
-    select2-rails (3.4.3)
237
-      sass-rails
230
+    safe_yaml (1.0.3)
231
+    sass (3.2.19)
232
+    sass-rails (4.0.3)
233
+      railties (>= 4.0.0, < 5.0)
234
+      sass (~> 3.2.0)
235
+      sprockets (~> 2.8, <= 2.11.0)
236
+      sprockets-rails (~> 2.0)
237
+    select2-rails (3.5.4)
238 238
       thor (~> 0.14)
239
-    shoulda-matchers (2.2.0)
239
+    shoulda-matchers (2.6.0)
240 240
       activesupport (>= 3.0.0)
241 241
     simple_oauth (0.2.0)
242 242
     simplecov (0.8.2)
@@ -244,32 +244,34 @@ GEM
244 244
       multi_json
245 245
       simplecov-html (~> 0.8.0)
246 246
     simplecov-html (0.8.0)
247
-    slop (3.4.5)
248
-    sprockets (2.2.2)
247
+    slop (3.5.0)
248
+    sprockets (2.11.0)
249 249
       hike (~> 1.2)
250 250
       multi_json (~> 1.0)
251 251
       rack (~> 1.0)
252 252
       tilt (~> 1.1, != 1.3.0)
253
-    system_timer (1.2.4)
253
+    sprockets-rails (2.1.3)
254
+      actionpack (>= 3.0)
255
+      activesupport (>= 3.0)
256
+      sprockets (~> 2.8)
254 257
     systemu (2.6.4)
255
-    term-ansicolor (1.2.2)
256
-      tins (~> 0.8)
257
-    therubyracer (0.12.0)
258
+    term-ansicolor (1.3.0)
259
+      tins (~> 1.0)
260
+    therubyracer (0.12.1)
258 261
       libv8 (~> 3.16.14.0)
259 262
       ref
260
-    thor (0.18.1)
261
-    thread_safe (0.1.3)
262
-      atomic
263
+    thor (0.19.1)
264
+    thread_safe (0.3.3)
263 265
     tilt (1.4.1)
264
-    tins (0.13.1)
266
+    tins (1.1.0)
265 267
     treetop (1.4.15)
266 268
       polyglot
267 269
       polyglot (>= 0.3.1)
268
-    twilio-ruby (3.10.0)
270
+    twilio-ruby (3.11.5)
269 271
       builder (>= 2.1.2)
270 272
       jwt (>= 0.1.2)
271 273
       multi_json (>= 1.3.0)
272
-    twitter (5.7.1)
274
+    twitter (5.8.0)
273 275
       addressable (~> 2.3)
274 276
       buftok (~> 0.2.0)
275 277
       equalizer (~> 0.0.9)
@@ -280,25 +282,26 @@ GEM
280 282
       memoizable (~> 0.4.0)
281 283
       naught (~> 1.0)
282 284
       simple_oauth (~> 0.2.0)
283
-    typhoeus (0.6.3)
284
-      ethon (~> 0.5.11)
285
-    tzinfo (0.3.38)
286
-    uglifier (2.1.2)
285
+    typhoeus (0.6.8)
286
+      ethon (>= 0.7.0)
287
+    tzinfo (1.1.0)
288
+      thread_safe (~> 0.1)
289
+    uglifier (2.5.0)
287 290
       execjs (>= 0.3.0)
288
-      multi_json (~> 1.0, >= 1.0.2)
291
+      json (>= 1.8.0)
289 292
     uuid (2.3.7)
290 293
       macaddr (~> 1.0)
291 294
     warden (1.2.3)
292 295
       rack (>= 1.0)
293
-    webmock (1.13.0)
296
+    webmock (1.17.4)
294 297
       addressable (>= 2.2.7)
295 298
       crack (>= 0.3.2)
296
-    weibo_2 (0.1.4)
299
+    weibo_2 (0.1.6)
297 300
       hashie (~> 2.0.4)
298
-      multi_json (~> 1.7.2)
301
+      multi_json (~> 1)
299 302
       oauth2 (~> 0.9.1)
300 303
       rest-client (~> 1.6.7)
301
-    wunderground (1.1.0)
304
+    wunderground (1.2.0)
302 305
       addressable
303 306
       httparty (> 0.6.0)
304 307
       json (> 1.4.0)
@@ -308,49 +311,55 @@ PLATFORMS
308 311
   ruby
309 312
 
310 313
 DEPENDENCIES
311
-  ace-rails-ap
314
+  ace-rails-ap (~> 2.0.1)
312 315
   better_errors
313 316
   binding_of_caller
314
-  bootstrap-kaminari-views
315
-  coffee-rails (~> 3.2.1)
317
+  bootstrap-kaminari-views (~> 0.0.2)
318
+  coffee-rails (~> 4.0.0)
316 319
   coveralls
317
-  daemons
318
-  delayed_job
319
-  delayed_job_active_record
320
-  devise
320
+  daemons (~> 1.1.9)
321
+  delayed_job (~> 4.0.0)
322
+  delayed_job_active_record (~> 4.0.0)
323
+  delorean
324
+  devise (~> 3.2.4)
325
+  dotenv-deployment
321 326
   dotenv-rails
322
-  em-http-request
323
-  fastercsv
324
-  forecast_io
325
-  foreman
326
-  geokit-rails3
327
-  jquery-rails
328
-  json (>= 1.7.7)
329
-  jsonpath
330
-  kaminari
331
-  kramdown
332
-  mysql2
333
-  nokogiri
327
+  em-http-request (~> 1.1.2)
328
+  faraday (~> 0.9.0)
329
+  faraday_middleware
330
+  forecast_io (~> 2.0.0)
331
+  foreman (~> 0.63.0)
332
+  geokit (~> 1.8.4)
333
+  geokit-rails (~> 2.0.1)
334
+  hipchat (~> 1.1.0)
335
+  jquery-rails (~> 3.1.0)
336
+  json (~> 1.8.1)
337
+  jsonpath (~> 0.5.3)
338
+  kaminari (~> 0.15.1)
339
+  kramdown (~> 1.3.3)
340
+  mysql2 (~> 0.3.15)
341
+  nokogiri (~> 1.6.1)
342
+  protected_attributes (~> 1.0.7)
334 343
   pry
335
-  rails
336
-  rake
344
+  rack
345
+  rails (= 4.1.0)
337 346
   rr
338 347
   rspec
339 348
   rspec-rails
340
-  rturk
341
-  ruby-growl
342
-  rufus-scheduler
343
-  sass-rails (~> 3.2.3)
344
-  select2-rails
349
+  rturk (~> 2.12.1)
350
+  ruby-growl (~> 4.1.0)
351
+  rufus-scheduler (~> 3.0.7)
352
+  sass-rails (~> 4.0.0)
353
+  select2-rails (~> 3.5.4)
345 354
   shoulda-matchers
346
-  system_timer
347
-  therubyracer
348
-  twilio-ruby
349
-  twitter (~> 5.7.1)
355
+  therubyracer (~> 0.12.1)
356
+  twilio-ruby (~> 3.11.5)
357
+  twitter (~> 5.8.0)
350 358
   twitter-stream!
351
-  typhoeus
352
-  uglifier (>= 1.0.3)
359
+  typhoeus (~> 0.6.3)
360
+  tzinfo-data
361
+  uglifier (>= 1.3.0)
353 362
   webmock
354
-  weibo_2
355
-  wunderground
363
+  weibo_2 (~> 0.1.4)
364
+  wunderground (~> 1.2.0)
356 365
   xmpp4r (~> 0.5.6)

+ 1 - 1
Procfile

@@ -8,4 +8,4 @@ dj: bundle exec script/delayed_job run
8 8
 # web: bundle exec unicorn -c config/unicorn/production.rb
9 9
 # schedule: bundle exec rails runner bin/schedule.rb
10 10
 # twitter: bundle exec rails runner bin/twitter_stream.rb
11
-# dj: bundle exec script/delayed_job run
11
+# dj: bundle exec script/delayed_job run

+ 9 - 24
README.md

@@ -24,7 +24,7 @@ Follow [@tectonic](https://twitter.com/tectonic) for updates as Huginn evolves,
24 24
 
25 25
 ### We need your help!
26 26
 
27
-Want to help with Huginn?  Try tackling [issues tagged with #help-wanted](https://github.com/cantino/huginn/issues?direction=desc&labels=help-wanted&page=1&sort=created&state=open).
27
+Want to help with Huginn?  All contributions are encouraged!  You could make UI improvements, add new Agents, write documentation and tutorials, or try tackling [issues tagged with #help-wanted](https://github.com/cantino/huginn/issues?direction=desc&labels=help-wanted&page=1&sort=created&state=open).
28 28
 
29 29
 ## Examples
30 30
 
@@ -49,44 +49,29 @@ And now, some example screenshots.  Below them are instructions to get you start
49 49
 If you just want to play around, you can simply clone this repository, then perform the following steps:
50 50
 
51 51
 * Copy `.env.example` to `.env` (`cp .env.example .env`) and edit `.env`, at least updating the `APP_SECRET_TOKEN` variable.
52
-* Run `rake db:create`, `rake db:migrate`, and then `rake db:seed` to create a development MySQL database with some example seed data.
52
+* Run `rake db:create`, `rake db:migrate`, and then `rake db:seed` to create a development MySQL database with some example Agents.
53 53
 * Run `foreman start`, visit [http://localhost:3000/][localhost], and login with the username of `admin` and the password of `password`.
54 54
 * Setup some Agents!
55
+* Read the [wiki][wiki] for usage examples and to get started making new Agents.
55 56
 
56 57
 Note: by default, emails are not sent in the `development` Rails environment, which is what you just setup.  If you'd like to enable emails when playing with Huginn locally, edit `config.action_mailer.perform_deliveries` in `config/environments/development.rb`.
57 58
 
58 59
 If you need more detailed instructions, see the [Novice setup guide][novice-setup-guide].
59 60
 
60 61
 [localhost]: http://localhost:3000/
62
+[wiki]: https://github.com/cantino/huginn/wiki
61 63
 [novice-setup-guide]: https://github.com/cantino/huginn/wiki/Novice-setup-guide
62 64
 
63
-### Real Start
64
-
65
-Follow these instructions if you wish to deploy your own version of Huginn or contribute back to the project.  Github doesn't make it easy to work with private forks of public repositories, so I recommend that you follow the following steps:
66
-
67
-* Make a public fork of Huginn. If you can't create private Github repositories, you can skip the steps below. Just follow the *Quick Start* steps above and make pull requests when you want to contribute a patch. 
68
-* Make a private, empty Github repository called `huginn-private`
69
-* Duplicate your public fork into your new private repository (via [Github's instructions](https://help.github.com/articles/duplicating-a-repository)):
70
-
71
-        git clone --bare git@github.com:you/huginn.git
72
-        cd huginn.git
73
-        git push --mirror git@github.com:you/huginn-private.git
74
-        cd .. && rm -rf huginn.git
75
-
76
-* Checkout your new private repository.
77
-* Add your Huginn public fork as a remote to your new private repository (`huginn-private`):
78
-
79
-        git remote add public git@github.com:you/huginn.git
80
-
81
-* Run the steps from *Quick Start* above to configure your copy of Huginn.
82
-* When you want to contribute patches, do a remote push from your private repository to your public fork of the relevant commits, then make a pull request to this repository.
83
-
84 65
 ## Deployment
85 66
 
86 67
 Please see [the Huginn Wiki](https://github.com/cantino/huginn/wiki#deploying-huginn) for detailed deployment strategies for different providers.
87 68
 
88 69
 ### Optional Setup
89 70
 
71
+#### Setup for private development
72
+
73
+See [private development instructions](https://github.com/cantino/huginn/wiki/Private-development-instructions) on the wiki.
74
+
90 75
 #### Enable the WeatherAgent
91 76
 
92 77
 In order to use the WeatherAgent you need an [API key with Wunderground](http://www.wunderground.com/weather/api/). Signup for one and then change value of `api_key: your-key` in your seeded WeatherAgent.
@@ -119,5 +104,5 @@ Huginn is a work in progress and is hopefully just getting started.  Please get
119 104
 
120 105
 Please fork, add specs, and send pull requests!
121 106
 
122
-[![Build Status](https://travis-ci.org/cantino/huginn.png)](https://travis-ci.org/cantino/huginn) [![Coverage Status](https://coveralls.io/repos/cantino/huginn/badge.png)](https://coveralls.io/r/cantino/huginn) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/cantino/huginn/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
107
+[![Build Status](https://travis-ci.org/cantino/huginn.png)](https://travis-ci.org/cantino/huginn) [![Coverage Status](https://coveralls.io/repos/cantino/huginn/badge.png)](https://coveralls.io/r/cantino/huginn) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/cantino/huginn/trend.png)](https://bitdeli.com/free "Bitdeli Badge") [![Dependency Status](https://gemnasium.com/cantino/huginn.svg)](https://gemnasium.com/cantino/huginn)
123 108
 

+ 3 - 3
app/assets/javascripts/application.js.coffee.erb

@@ -56,9 +56,6 @@ $(document).ready ->
56 56
   # JSON Editor
57 57
   window.jsonEditor = setupJsonEditor()
58 58
 
59
-  # Select2 Selects
60
-  $(".select2").select2(width: 'resolve')
61
-
62 59
   # Flash
63 60
   if $(".flash").length
64 61
     setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000)
@@ -155,6 +152,9 @@ $(document).ready ->
155 152
 
156 153
   $("#agent_type").change() if $("#agent_type").length
157 154
 
155
+  # Select2 Selects
156
+  $(".select2").select2(width: 'resolve')
157
+
158 158
   if $(".schedule-region")
159 159
     if $(".schedule-region").data("can-be-scheduled") == true
160 160
       showSchedule()

+ 39 - 0
app/concerns/json_path_options_overwritable.rb

@@ -0,0 +1,39 @@
1
+module JsonPathOptionsOverwritable
2
+  extend ActiveSupport::Concern
3
+  # Using this concern allows providing optional `<attribute>_path` options hash
4
+  # attributes which will then (if not blank) be interpolated using the provided JSONPath.
5
+  #
6
+  # Example options Hash:
7
+  # {
8
+  #   name: 'Huginn',
9
+  #   name_path: '$.name',
10
+  #   title: 'Hello from Huginn'
11
+  #   title_path: ''
12
+  # }
13
+  # Example event payload:
14
+  # {
15
+  #   name: 'dynamic huginn'
16
+  # }
17
+  # calling agent.merge_json_path_options(event) returns the following hash:
18
+  # {
19
+  #   name: 'dynamic huginn'
20
+  #   title: 'Hello from Huginn'
21
+  # }
22
+
23
+  private
24
+  def merge_json_path_options(event)
25
+    options.select { |k, v| options_with_path.include? k}.tap do |merged_options|
26
+      options_with_path.each do |a|
27
+        merged_options[a] = select_option(event, a)
28
+      end
29
+    end
30
+  end
31
+
32
+  def select_option(event, a)
33
+    if options[a.to_s + '_path'].present?
34
+      Utils.value_at(event.payload, options[a.to_s + '_path'])
35
+    else
36
+      options[a]
37
+    end
38
+  end
39
+end

+ 15 - 0
app/concerns/working_helpers.rb

@@ -0,0 +1,15 @@
1
+module WorkingHelpers
2
+  extend ActiveSupport::Concern
3
+
4
+  def event_created_within?(days)
5
+    last_event_at && last_event_at > days.to_i.days.ago
6
+  end
7
+
8
+  def recent_error_logs?
9
+    last_event_at && last_error_log_at && last_error_log_at > (last_event_at - 2.minutes)
10
+  end
11
+
12
+  def received_event_without_error?
13
+    (last_receive_at.present? && last_error_log_at.blank?) || (last_receive_at.present? && last_error_log_at.present? && last_receive_at > last_error_log_at)
14
+  end
15
+end

+ 9 - 1
app/controllers/agents_controller.rb

@@ -1,4 +1,6 @@
1 1
 class AgentsController < ApplicationController
2
+  include DotHelper
3
+
2 4
   def index
3 5
     @agents = current_user.agents.page(params[:page])
4 6
 
@@ -69,7 +71,13 @@ class AgentsController < ApplicationController
69 71
   end
70 72
 
71 73
   def new
72
-    @agent = current_user.agents.build
74
+    agents = current_user.agents
75
+
76
+    if id = params[:id]
77
+      @agent = agents.build_clone(agents.find(id))
78
+    else
79
+      @agent = agents.build
80
+    end
73 81
 
74 82
     respond_to do |format|
75 83
       format.html

+ 40 - 0
app/helpers/dot_helper.rb

@@ -0,0 +1,40 @@
1
+module DotHelper
2
+  def render_agents_diagram(agents)
3
+    if (command = ENV['USE_GRAPHVIZ_DOT']) &&
4
+       (svg = IO.popen([command, *%w[-Tsvg -q1 -o/dev/stdout /dev/stdin]], 'w+') { |dot|
5
+          dot.print agents_dot(agents, true)
6
+          dot.close_write
7
+          dot.read
8
+        } rescue false)
9
+      svg.html_safe
10
+    else
11
+      tag('img', src: URI('https://chart.googleapis.com/chart').tap { |uri|
12
+            uri.query = URI.encode_www_form(cht: 'gv', chl: agents_dot(agents))
13
+          })
14
+    end
15
+  end
16
+
17
+  private
18
+
19
+  def dot_id(string)
20
+    # Backslash escaping seems to work for the backslash itself,
21
+    # despite the DOT language document.
22
+    '"%s"' % string.gsub(/\\/, "\\\\\\\\").gsub(/"/, "\\\\\"")
23
+  end
24
+
25
+  def agents_dot(agents, rich = false)
26
+    "digraph foo {".tap { |dot|
27
+      agents.each.with_index do |agent, index|
28
+        if rich
29
+          dot << '%s[URL=%s];' % [dot_id(agent.name), dot_id(agent_path(agent.id))]
30
+        else
31
+          dot << '%s;' % dot_id(agent.name)
32
+        end
33
+        agent.receivers.each do |receiver|
34
+          dot << "%s->%s;" % [dot_id(agent.name), dot_id(receiver.name)]
35
+        end
36
+      end
37
+      dot << "}"
38
+    }
39
+  end
40
+end

+ 37 - 27
app/models/agent.rb

@@ -11,12 +11,13 @@ class Agent < ActiveRecord::Base
11 11
   include MarkdownClassAttributes
12 12
   include JSONSerializedField
13 13
   include RDBMSFunctions
14
+  include WorkingHelpers
14 15
 
15 16
   markdown_class_attributes :description, :event_description
16 17
 
17 18
   load_types_in "Agents"
18 19
 
19
-  SCHEDULES = %w[every_2m every_5m every_10m every_30m every_1h every_2h every_5h every_12h every_1d every_2d every_7d
20
+  SCHEDULES = %w[every_1m every_2m every_5m every_10m every_30m every_1h every_2h every_5h every_12h every_1d every_2d every_7d
20 21
                  midnight 1am 2am 3am 4am 5am 6am 7am 8am 9am 10am 11am noon 1pm 2pm 3pm 4pm 5pm 6pm 7pm 8pm 9pm 10pm 11pm never]
21 22
 
22 23
   EVENT_RETENTION_SCHEDULES = [["Forever", 0], ["1 day", 1], *([2, 3, 4, 5, 7, 14, 21, 30, 45, 90, 180, 365].map {|n| ["#{n} days", n] })]
@@ -39,10 +40,10 @@ class Agent < ActiveRecord::Base
39 40
   after_save :possibly_update_event_expirations
40 41
 
41 42
   belongs_to :user, :inverse_of => :agents
42
-  has_many :events, :dependent => :delete_all, :inverse_of => :agent, :order => "events.id desc"
43
+  has_many :events, -> { order("events.id desc") }, :dependent => :delete_all, :inverse_of => :agent
43 44
   has_one  :most_recent_event, :inverse_of => :agent, :class_name => "Event", :order => "events.id desc"
44
-  has_many :logs, :dependent => :delete_all, :inverse_of => :agent, :class_name => "AgentLog", :order => "agent_logs.id desc"
45
-  has_many :received_events, :through => :sources, :class_name => "Event", :source => :events, :order => "events.id desc"
45
+  has_many :logs,  -> { order("agent_logs.id desc") }, :dependent => :delete_all, :inverse_of => :agent, :class_name => "AgentLog"
46
+  has_many :received_events, -> { order("events.id desc") }, :through => :sources, :class_name => "Event", :source => :events
46 47
   has_many :links_as_source, :dependent => :delete_all, :foreign_key => "source_id", :class_name => "Link", :inverse_of => :source
47 48
   has_many :links_as_receiver, :dependent => :delete_all, :foreign_key => "receiver_id", :class_name => "Link", :inverse_of => :receiver
48 49
   has_many :sources, :through => :links_as_receiver, :class_name => "Agent", :inverse_of => :receivers
@@ -83,18 +84,6 @@ class Agent < ActiveRecord::Base
83 84
     raise "Implement me in your subclass"
84 85
   end
85 86
 
86
-  def validate_options
87
-    # Implement me in your subclass to test for valid options.
88
-  end
89
-
90
-  def event_created_within?(days)
91
-    last_event_at && last_event_at > days.to_i.days.ago
92
-  end
93
-
94
-  def recent_error_logs?
95
-    last_event_at && last_error_log_at && last_error_log_at > (last_event_at - 2.minutes)
96
-  end
97
-
98 87
   def create_event(attrs)
99 88
     if can_create_events?
100 89
       events.create!({ 
@@ -193,17 +182,7 @@ class Agent < ActiveRecord::Base
193 182
     update_column :last_error_log_at, nil
194 183
   end
195 184
 
196
-  # Validations and Callbacks
197
-
198
-  def sources_are_owned
199
-    errors.add(:sources, "must be owned by you") unless sources.all? {|s| s.user == user }
200
-  end
201
-
202
-  def validate_schedule
203
-    unless cannot_be_scheduled?
204
-      errors.add(:schedule, "is not a valid schedule") unless SCHEDULES.include?(schedule.to_s)
205
-    end
206
-  end
185
+  # Callbacks
207 186
 
208 187
   def set_default_schedule
209 188
     self.schedule = default_schedule unless schedule.present? || cannot_be_scheduled?
@@ -222,10 +201,41 @@ class Agent < ActiveRecord::Base
222 201
   def possibly_update_event_expirations
223 202
     update_event_expirations! if keep_events_for_changed?
224 203
   end
204
+  
205
+  #Validation Methods
206
+  
207
+  private
208
+  
209
+  def sources_are_owned
210
+    errors.add(:sources, "must be owned by you") unless sources.all? {|s| s.user == user }
211
+  end
212
+  
213
+  def validate_schedule
214
+    unless cannot_be_scheduled?
215
+      errors.add(:schedule, "is not a valid schedule") unless SCHEDULES.include?(schedule.to_s)
216
+    end
217
+  end
218
+  
219
+  def validate_options
220
+    # Implement me in your subclass to test for valid options.
221
+  end
225 222
 
226 223
   # Class Methods
227 224
 
228 225
   class << self
226
+    def build_clone(original)
227
+      new(original.slice(:type, :options, :schedule, :source_ids, :keep_events_for, :propagate_immediately)) { |clone|
228
+        # Give it a unique name
229
+        2.upto(count) do |i|
230
+          name = '%s (%d)' % [original.name, i]
231
+          unless exists?(name: name)
232
+            clone.name = name
233
+            break
234
+          end
235
+        end
236
+      }
237
+    end
238
+
229 239
     def cannot_be_scheduled!
230 240
       @cannot_be_scheduled = true
231 241
     end

+ 90 - 0
app/models/agents/basecamp_agent.rb

@@ -0,0 +1,90 @@
1
+module Agents
2
+  class BasecampAgent < Agent
3
+    cannot_receive_events!
4
+
5
+    description <<-MD
6
+      The BasecampAgent checks a Basecamp project for new Events
7
+
8
+      It is required that you enter your Basecamp credentials (`username` and `password`).
9
+
10
+      You also need to provide your Basecamp `user_id` and the `project_id` of the project you want to monitor.
11
+      If you have your Basecamp project opened in your browser you can find the user_id and project_id as follows:
12
+
13
+      `https://basecamp.com/`
14
+      user_id
15
+      `/projects/`
16
+      project_id
17
+      `-explore-basecamp`
18
+    MD
19
+
20
+    event_description <<-MD
21
+      Events are the raw JSON provided by the Basecamp API. Should look something like:
22
+
23
+        {
24
+          "creator": {
25
+            "fullsize_avatar_url": "https://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/original.gif?r=3",
26
+            "avatar_url": "http://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/avatar.gif?r=3",
27
+            "name": "Dominik Sander",
28
+            "id": 123456
29
+          },
30
+          "attachments": [],
31
+          "raw_excerpt": "test test",
32
+          "excerpt": "test test",
33
+          "id": 6454342343,
34
+          "created_at": "2014-04-17T10:25:31.000+02:00",
35
+          "updated_at": "2014-04-17T10:25:31.000+02:00",
36
+          "summary": "commented on whaat",
37
+          "action": "commented on",
38
+          "target": "whaat",
39
+          "url": "https://basecamp.com/12456/api/v1/projects/76454545-explore-basecamp/messages/76454545-whaat.json",
40
+          "html_url": "https://basecamp.com/12456/projects/76454545-explore-basecamp/messages/76454545-whaat#comment_76454545"
41
+        }
42
+    MD
43
+
44
+    default_schedule "every_10m"
45
+
46
+    def default_options
47
+      {
48
+        'username' => '',
49
+        'password' => '',
50
+        'user_id' => '',
51
+        'project_id' => '',
52
+      }
53
+    end
54
+
55
+    def validate_options
56
+      errors.add(:base, "you need to specify your basecamp username") unless options['username'].present?
57
+      errors.add(:base, "you need to specify your basecamp password") unless options['password'].present?
58
+      errors.add(:base, "you need to specify your basecamp user id") unless options['user_id'].present?
59
+      errors.add(:base, "you need to specify the basecamp project id of which you want to receive events") unless options['project_id'].present?
60
+    end
61
+
62
+    def working?
63
+      (events_count.present? && events_count > 0)
64
+    end
65
+
66
+    def check
67
+      reponse = HTTParty.get request_url, request_options.merge(query_parameters)
68
+      memory[:last_run] = Time.now.utc.iso8601
69
+      if last_check_at != nil
70
+        JSON.parse(reponse.body).each do |event|
71
+          create_event :payload => event
72
+        end
73
+      end
74
+      save!
75
+    end
76
+
77
+  private
78
+    def request_url
79
+      "https://basecamp.com/#{URI.encode(options[:user_id].to_s)}/api/v1/projects/#{URI.encode(options[:project_id].to_s)}/events.json"
80
+    end
81
+
82
+    def request_options
83
+      {:basic_auth => {:username =>options[:username], :password=>options[:password]}, :headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)"}}
84
+    end
85
+
86
+    def query_parameters
87
+      memory[:last_run].present? ? { :query => {:since => memory[:last_run]} } : {}
88
+    end
89
+  end
90
+end

+ 119 - 3
app/models/agents/event_formatting_agent.rb

@@ -12,6 +12,10 @@ module Agents
12 12
               "celsius": "18",
13 13
               "fahreinheit": "64"
14 14
             },
15
+            "date": {
16
+              "epoch": "1357959600",
17
+              "pretty": "10:00 PM EST on January 11, 2013"
18
+            },
15 19
             "conditions": "Rain showers",
16 20
             "data": "This is some data"
17 21
           }
@@ -33,6 +37,33 @@ module Agents
33 37
             "subject": "This is some data"
34 38
           }
35 39
 
40
+      In `matchers` setting you can perform regular expression matching against contents of events and expand the match data for use in `instructions` setting.  Here is an example:
41
+
42
+          {
43
+            "matchers": [
44
+              {
45
+                "path": "$.date.pretty",
46
+                "regexp": "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)",
47
+                "to": "pretty_date",
48
+              }
49
+            ]
50
+          }
51
+
52
+      This virtually merges the following hash into the original event hash:
53
+
54
+          "pretty_date": {
55
+            "time": "10:00 PM EST",
56
+            "0": "10:00 PM EST on January 11, 2013"
57
+            "1": "10:00 PM EST",
58
+          }
59
+
60
+      So you can use it in `instructions` like this:
61
+
62
+          "instructions": {
63
+            "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius according to the forecast at <$.pretty_date.time>.",
64
+            "subject": "$.data"
65
+          }
66
+
36 67
       If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`.
37 68
 
38 69
       By default, the output event will have `agent` and `created_at` fields added as well, reflecting the original Agent type and Event creation time.  You can skip these outputs by setting `skip_agent` and `skip_created_at` to `true`.
@@ -46,8 +77,12 @@ module Agents
46 77
 
47 78
     event_description "User defined"
48 79
 
80
+    after_save :clear_matchers
81
+
49 82
     def validate_options
50 83
       errors.add(:base, "instructions, mode, skip_agent, and skip_created_at all need to be present.") unless options['instructions'].present? and options['mode'].present? and options['skip_agent'].present? and options['skip_created_at'].present?
84
+
85
+      validate_matchers
51 86
     end
52 87
 
53 88
     def default_options
@@ -56,6 +91,7 @@ module Agents
56 91
           'message' =>  "You received a text <$.text> from <$.fields.from>",
57 92
           'some_other_field' => "Looks like the weather is going to be <$.fields.weather>"
58 93
         },
94
+        'matchers' => [],
59 95
         'mode' => "clean",
60 96
         'skip_agent' => "false",
61 97
         'skip_created_at' => "false"
@@ -68,12 +104,92 @@ module Agents
68 104
 
69 105
     def receive(incoming_events)
70 106
       incoming_events.each do |event|
71
-        formatted_event = options['mode'].to_s == "merge" ? event.payload : {}
72
-        options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, event.payload) }
107
+        formatted_event = options['mode'].to_s == "merge" ? event.payload.dup : {}
108
+        payload = perform_matching(event.payload)
109
+        options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, payload) }
73 110
         formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless options['skip_agent'].to_s == "true"
74 111
         formatted_event['created_at'] = event.created_at unless options['skip_created_at'].to_s == "true"
75 112
         create_event :payload => formatted_event
76 113
       end
77 114
     end
115
+
116
+    private
117
+
118
+    def validate_matchers
119
+      matchers = options['matchers'] or return
120
+
121
+      unless matchers.is_a?(Array)
122
+        errors.add(:base, "matchers must be an array if present")
123
+        return
124
+      end
125
+
126
+      matchers.each do |matcher|
127
+        unless matcher.is_a?(Hash)
128
+          errors.add(:base, "each matcher must be a hash")
129
+          next
130
+        end
131
+
132
+        regexp, path, to = matcher.values_at(*%w[regexp path to])
133
+
134
+        if regexp.present?
135
+          begin
136
+            Regexp.new(regexp)
137
+          rescue
138
+            errors.add(:base, "bad regexp found in matchers: #{regexp}")
139
+          end
140
+        else
141
+          errors.add(:base, "regexp is mandatory for a matcher and must be a string")
142
+        end
143
+
144
+        errors.add(:base, "path is mandatory for a matcher and must be a string") if !path.present?
145
+
146
+        errors.add(:base, "to must be a string if present in a matcher") if to.present? && !to.is_a?(String)
147
+      end
148
+    end
149
+
150
+    def perform_matching(payload)
151
+      matchers.inject(payload.dup) { |hash, matcher|
152
+        matcher[hash]
153
+      }
154
+    end
155
+
156
+    def matchers
157
+      @matchers ||=
158
+        if matchers = options['matchers']
159
+          matchers.map { |matcher|
160
+            regexp, path, to = matcher.values_at(*%w[regexp path to])
161
+            re = Regexp.new(regexp)
162
+            proc { |hash|
163
+              mhash = {}
164
+              value = Utils.value_at(hash, path)
165
+              if value.is_a?(String) && (m = re.match(value))
166
+                m.to_a.each_with_index { |s, i|
167
+                  mhash[i.to_s] = s
168
+                }
169
+                m.names.each do |name|
170
+                  mhash[name] = m[name]
171
+                end if m.respond_to?(:names)
172
+              end
173
+              if to
174
+                case value = hash[to]
175
+                when Hash
176
+                  value.update(mhash)
177
+                else
178
+                  hash[to] = mhash
179
+                end
180
+              else
181
+                hash.update(mhash)
182
+              end
183
+              hash
184
+            }
185
+          }
186
+        else
187
+          []
188
+        end
189
+    end
190
+
191
+    def clear_matchers
192
+      @matchers = nil
193
+    end
78 194
   end
79
-end
195
+end

+ 62 - 0
app/models/agents/hipchat_agent.rb

@@ -0,0 +1,62 @@
1
+module Agents
2
+  class HipchatAgent < Agent
3
+    include JsonPathOptionsOverwritable
4
+
5
+    cannot_be_scheduled!
6
+    cannot_create_events!
7
+
8
+    description <<-MD
9
+      The HipchatAgent sends messages to a Hipchat Room
10
+
11
+      To authenticate you need to set the `auth_token`, you can get one at your Hipchat Group Admin page which you can find here:
12
+
13
+      `https://`yoursubdomain`.hipchat.com/admin/api`
14
+
15
+      Change the `room_name` to the name of the room you want to send notifications to.
16
+
17
+      You can provide a `username` and a `message`. When sending a HTML formatted message change `format` to "html".
18
+      If you want your message to notify the room members change `notify` to "true".
19
+      Modify the background color of your message via the `color` attribute (one of "yellow", "red", "green", "purple", "gray", or "random")
20
+
21
+      If you want to specify either of those attributes per event, you can provide a [JSONPath](http://goessner.net/articles/JsonPath/) for each of them (except the `auth_token`).
22
+    MD
23
+
24
+    def default_options
25
+      {
26
+        'auth_token' => '',
27
+        'room_name' => '',
28
+        'room_name_path' => '',
29
+        'username' => "Huginn",
30
+        'username_path' => '',
31
+        'message' => "Hello from Huginn!",
32
+        'message_path' => '',
33
+        'notify' => false,
34
+        'notify_path' => '',
35
+        'color' => 'yellow',
36
+        'color_path' => '',
37
+      }
38
+    end
39
+
40
+    def validate_options
41
+      errors.add(:base, "you need to specify a hipchat auth_token") unless options['auth_token'].present?
42
+      errors.add(:base, "you need to specify a room_name or a room_name_path") if options['room_name'].blank? && options['room_name_path'].blank?
43
+    end
44
+
45
+    def working?
46
+      (last_receive_at.present? && last_error_log_at.nil?) || (last_receive_at.present? && last_error_log_at.present? && last_receive_at > last_error_log_at)
47
+    end
48
+
49
+    def receive(incoming_events)
50
+      client = HipChat::Client.new(options[:auth_token])
51
+      incoming_events.each do |event|
52
+        mo = merge_json_path_options event
53
+        client[mo[:room_name]].send(mo[:username], mo[:message], :notify => mo[:notify].to_s == 'true' ? 1 : 0, :color => mo[:color])
54
+      end
55
+    end
56
+
57
+    private
58
+    def options_with_path
59
+      [:room_name, :username, :message, :notify, :color]
60
+    end
61
+  end
62
+end

+ 68 - 10
app/models/agents/post_agent.rb

@@ -1,10 +1,15 @@
1 1
 module Agents
2 2
   class PostAgent < Agent
3
-    cannot_be_scheduled!
4 3
     cannot_create_events!
5 4
 
5
+    default_schedule "never"
6
+
6 7
     description <<-MD
7
-       Post Agent receives events from other agents and send those events as the contents of a post request to a specified url. `post_url` field must specify where you would like to receive post requests and do not forget to include URI scheme (`http` or `https`)
8
+      A PostAgent receives events from other agents (or runs periodically), merges those events with the contents of `payload`, and sends the results as POST (or GET) requests to a specified url.
9
+
10
+      The `post_url` field must specify where you would like to send requests. Please include the URI scheme (`http` or `https`).
11
+
12
+      The `headers` field is optional.  When present, it should be a hash of headers to send with the request.
8 13
     MD
9 14
 
10 15
     event_description "Does not produce events."
@@ -12,7 +17,12 @@ module Agents
12 17
     def default_options
13 18
       {
14 19
         'post_url' => "http://www.example.com",
15
-        'expected_receive_period_in_days' => 1
20
+        'expected_receive_period_in_days' => 1,
21
+        'method' => 'post',
22
+        'payload' => {
23
+          'key' => 'value'
24
+        },
25
+        'headers' => {}
16 26
       }
17 27
     end
18 28
 
@@ -20,23 +30,71 @@ module Agents
20 30
       last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs?
21 31
     end
22 32
 
33
+    def method
34
+      (options['method'].presence || 'post').to_s.downcase
35
+    end
36
+
37
+    def headers
38
+      options['headers'].presence || {}
39
+    end
40
+
23 41
     def validate_options
24 42
       unless options['post_url'].present? && options['expected_receive_period_in_days'].present?
25 43
         errors.add(:base, "post_url and expected_receive_period_in_days are required fields")
26 44
       end
27
-    end
28 45
 
29
-    def post_event(uri, event)
30
-      req = Net::HTTP::Post.new(uri.request_uri)
31
-      req.form_data = event
32
-      Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == "https") { |http| http.request(req) }
46
+      if options['payload'].present? && !options['payload'].is_a?(Hash)
47
+        errors.add(:base, "if provided, payload must be a hash")
48
+      end
49
+
50
+      unless %w[post get].include?(method)
51
+        errors.add(:base, "method must be 'post' or 'get'")
52
+      end
53
+
54
+      unless headers.is_a?(Hash)
55
+        errors.add(:base, "if provided, headers must be a hash")
56
+      end
33 57
     end
34 58
 
35 59
     def receive(incoming_events)
36 60
       incoming_events.each do |event|
37
-        uri = URI options[:post_url]
38
-        post_event uri, event.payload
61
+        handle (options['payload'].presence || {}).merge(event.payload)
39 62
       end
40 63
     end
64
+
65
+    def check
66
+      handle options['payload'].presence || {}
67
+    end
68
+
69
+    def generate_uri(params = nil)
70
+      uri = URI options[:post_url]
71
+      uri.query = URI.encode_www_form(Hash[URI.decode_www_form(uri.query || '')].merge(params)) if params
72
+      uri
73
+    end
74
+
75
+    private
76
+
77
+    def handle(data)
78
+      if method == 'post'
79
+        post_data(data)
80
+      elsif method == 'get'
81
+        get_data(data)
82
+      else
83
+        error "Invalid method '#{method}'"
84
+      end
85
+    end
86
+
87
+    def post_data(data)
88
+      uri = generate_uri
89
+      req = Net::HTTP::Post.new(uri.request_uri, headers)
90
+      req.form_data = data
91
+      Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == "https") { |http| http.request(req) }
92
+    end
93
+
94
+    def get_data(data)
95
+      uri = generate_uri(data)
96
+      req = Net::HTTP::Get.new(uri.request_uri, headers)
97
+      Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == "https") { |http| http.request(req) }
98
+    end
41 99
   end
42 100
 end

+ 67 - 0
app/models/agents/pushbullet_agent.rb

@@ -0,0 +1,67 @@
1
+module Agents
2
+  class PushbulletAgent < Agent
3
+    include JsonPathOptionsOverwritable
4
+
5
+    cannot_be_scheduled!
6
+    cannot_create_events!
7
+
8
+    description <<-MD
9
+      The Pushbullet agent sends pushes to a pushbullet device
10
+
11
+      To authenticate you need to set the `api_key`, you can find yours at your account page:
12
+
13
+      `https://www.pushbullet.com/account`
14
+
15
+      Currently you need to get a the device identification manually:
16
+
17
+      `curl -u <your api key here>: https://api.pushbullet.com/api/devices`
18
+
19
+      Put one of the retured `iden` strings into the `device_id` field.
20
+
21
+      You can provide a `title` and a `body`.
22
+
23
+      If you want to specify `title` or `body` per event, you can provide a [JSONPath](http://goessner.net/articles/JsonPath/) for each of them.
24
+    MD
25
+
26
+    def default_options
27
+      {
28
+        'api_key' => '',
29
+        'device_id' => '',
30
+        'title' => "Hello from Huginn!",
31
+        'title_path' => '',
32
+        'body' => '',
33
+        'body_path' => '',
34
+      }
35
+    end
36
+
37
+    def validate_options
38
+      errors.add(:base, "you need to specify a pushbullet api_key") unless options['api_key'].present?
39
+      errors.add(:base, "you need to specify a device_id") if options['device_id'].blank?
40
+    end
41
+
42
+    def working?
43
+      received_event_without_error?
44
+    end
45
+
46
+    def receive(incoming_events)
47
+      incoming_events.each do |event|
48
+        response = HTTParty.post "https://api.pushbullet.com/api/pushes", query_options(event)
49
+        error(response.body) if response.body.include? 'error'
50
+      end
51
+    end
52
+
53
+    private
54
+    def query_options(event)
55
+      mo = merge_json_path_options event
56
+      basic_options.deep_merge(:body => {:title => mo[:title], :body => mo[:body]})
57
+    end
58
+
59
+    def basic_options
60
+      {:basic_auth => {:username =>options[:api_key], :password=>''}, :body => {:device_iden => options[:device_id], :type => 'note'}}
61
+    end
62
+
63
+    def options_with_path
64
+      [:title, :body]
65
+    end
66
+  end
67
+end

+ 107 - 0
app/models/agents/pushover_agent.rb

@@ -0,0 +1,107 @@
1
+module Agents
2
+  class PushoverAgent < Agent
3
+    cannot_be_scheduled!
4
+    cannot_create_events!
5
+
6
+    API_URL = 'https://api.pushover.net/1/messages.json'
7
+
8
+    description <<-MD
9
+      The PushoverAgent receives and collects events and sends them via push notification to a user/group.
10
+
11
+      **You need a Pushover API Token:** [https://pushover.net/apps/build](https://pushover.net/apps/build)
12
+
13
+      **You must provide** a `message` or `text` key that will contain the body of the notification. This can come from an event or be set as a default. Pushover API has a `512` Character Limit including `title`. `message` will be truncated.
14
+
15
+      * `token`: your application's API token
16
+      * `user`: the user or group key (not e-mail address).
17
+      * `expected_receive_period_in_days`:  is maximum number of days that you would expect to pass between events being received by this agent.
18
+
19
+      Your event can provide any of the following optional parameters or you can provide defaults:
20
+
21
+      * `device` - your user's device name to send the message directly to that device, rather than all of the user's devices
22
+      * `title` or `subject` - your notifications's title
23
+      * `url` - a supplementary URL to show with your message - `512` Character Limit
24
+      * `url_title` - a title for your supplementary URL, otherwise just the URL is shown - `100` Character Limit
25
+      * `priority` - send as `-1` to always send as a quiet notification, `0` is default, `1` to display as high-priority and bypass the user's quiet hours, or `2` for emergency priority: [Please read Pushover Docs on Emergency Priority](https://pushover.net/api#priority)
26
+      * `sound` - the name of one of the sounds supported by device clients to override the user's default sound choice. [See PushOver docs for sound options.](https://pushover.net/api#sounds)
27
+      * `retry` - Requred for emergency priority - Specifies how often (in seconds) the Pushover servers will send the same notification to the user. Minimum value: `30`
28
+      * `expire` - Requred for emergency priority - Specifies how many seconds your notification will continue to be retried for (every retry seconds). Maximum value: `86400`
29
+
30
+      Your event can also pass along a timestamp parameter:
31
+
32
+      * `timestamp` - a [Unix timestamp](https://en.wikipedia.org/wiki/Unix_time) of your message's date and time to display to the user, rather than the time your message is received by the Pushover API.
33
+
34
+    MD
35
+
36
+    def default_options
37
+      {
38
+        'token' => '',
39
+        'user' => '',
40
+        'message' => 'a default message',
41
+        'device' => '',
42
+        'title' => '',
43
+        'url' => '',
44
+        'url_title' => '',
45
+        'priority' => 0,
46
+        'sound' => 'pushover',
47
+        'retry' => 0,
48
+        'expire' => 0,
49
+        'expected_receive_period_in_days' => '1'
50
+      }
51
+    end
52
+
53
+    def validate_options
54
+      unless options['token'].present? && options['user'].present? && options['expected_receive_period_in_days'].present?
55
+        errors.add(:base, 'token, user, and expected_receive_period_in_days are all required.')
56
+      end
57
+    end
58
+
59
+    def receive(incoming_events)
60
+      incoming_events.each do |event|
61
+        message = (event.payload['message'].presence  || event.payload['text'].presence  || options['message']).to_s
62
+        if message.present?
63
+            post_params = {
64
+              'token' => options['token'],
65
+              'user' => options['user'],
66
+              'message' => message
67
+            }
68
+
69
+            post_params['device'] = event.payload['device'].presence  || options['device']
70
+            post_params['title'] = event.payload['title'].presence  || event.payload['subject'].presence  || options['title']
71
+
72
+            url = (event.payload['url'].presence  || options['url'] || '').to_s
73
+            url = url.slice 0..512
74
+            post_params['url'] = url
75
+
76
+            url_title = (event.payload['url_title'].presence  || options['url_title']).to_s
77
+            url_title = url_title.slice 0..100
78
+            post_params['url_title'] = url_title
79
+
80
+            post_params['priority'] = (event.payload['priority'].presence  || options['priority']).to_i
81
+
82
+            if event.payload.has_key? 'timestamp'
83
+              post_params['timestamp'] = (event.payload['timestamp']).to_s
84
+            end
85
+
86
+            post_params['sound'] = (event.payload['sound'].presence  || options['sound']).to_s
87
+
88
+            post_params['retry'] = (event.payload['retry'].presence  || options['retry']).to_i
89
+
90
+            post_params['expire'] = (event.payload['expire'].presence  || options['expire']).to_i
91
+
92
+            send_notification(post_params)
93
+        end
94
+      end
95
+    end
96
+
97
+    def working?
98
+      last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs?
99
+    end
100
+
101
+    def send_notification(post_params)
102
+      response = HTTParty.post(API_URL, :query => post_params)
103
+      puts response
104
+    end
105
+
106
+  end
107
+end

+ 111 - 0
app/models/agents/shell_command_agent.rb

@@ -0,0 +1,111 @@
1
+require 'open3'
2
+
3
+module Agents
4
+  class ShellCommandAgent < Agent
5
+    default_schedule "never"
6
+
7
+    def self.should_run?
8
+      ENV['ENABLE_INSECURE_AGENTS'] == "true"
9
+    end
10
+
11
+    description <<-MD
12
+      The ShellCommandAgent can execute commands on your local system, returning the output.
13
+
14
+      `command` specifies the command to be executed, and `path` will tell ShellCommandAgent in what directory to run this command.
15
+
16
+      `expected_update_period_in_days` is used to determine if the Agent is working.
17
+
18
+      ShellCommandAgent can also act upon received events. These events may contain their own `path` and `command` values. If they do not, ShellCommandAgent will use the configured options. For this reason, please specify defaults even if you are planning to have this Agent to respond to events.
19
+
20
+      The resulting event will contain the `command` which was executed, the `path` it was executed under, the `exit_status` of the command, the `errors`, and the actual `output`. ShellCommandAgent will not log an error if the result implies that something went wrong.
21
+
22
+      *Warning*: This type of Agent runs arbitrary commands on your system, #{Agents::ShellCommandAgent.should_run? ? "but is **currently enabled**" : "and is **currently disabled**"}.
23
+      Only enable this Agent if you trust everyone using your Huginn installation.
24
+      You can enable this Agent in your .env file by setting `ENABLE_INSECURE_AGENTS` to `true`.
25
+    MD
26
+
27
+    event_description <<-MD
28
+    Events look like this:
29
+
30
+      {
31
+        'command' => 'pwd',
32
+        'path' => '/home/Huginn',
33
+        'exit_status' => '0',
34
+        'errors' => '',
35
+        'output' => '/home/Huginn' 
36
+      }
37
+    MD
38
+
39
+    def default_options
40
+      {
41
+          'path' => "/",
42
+          'command' => "pwd",
43
+          'expected_update_period_in_days' => 1
44
+      }
45
+    end
46
+
47
+    def validate_options
48
+      unless options['path'].present? && options['command'].present? && options['expected_update_period_in_days'].present?
49
+        errors.add(:base, "The path, command, and expected_update_period_in_days fields are all required.")
50
+      end
51
+
52
+      unless File.directory?(options['path'])
53
+        errors.add(:base, "#{options['path']} is not a real directory.")
54
+      end
55
+    end
56
+
57
+    def working?
58
+      Agents::ShellCommandAgent.should_run? && event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs?
59
+    end
60
+
61
+    def receive(incoming_events)
62
+      incoming_events.each do |event|
63
+        handle(event.payload, event)
64
+      end
65
+    end
66
+
67
+    def check
68
+      handle(options)
69
+    end
70
+
71
+    private
72
+
73
+    def handle(opts = options, event = nil)
74
+      if Agents::ShellCommandAgent.should_run?
75
+        command = opts['command'] || options['command']
76
+        path = opts['path'] || options['path']
77
+
78
+        result, errors, exit_status = run_command(path, command)
79
+
80
+        vals = {"command" => command, "path" => path, "exit_status" => exit_status, "errors" => errors, "output" => result}
81
+        created_event = create_event :payload => vals
82
+
83
+        log("Ran '#{command}' under '#{path}'", :outbound_event => created_event, :inbound_event => event)
84
+      else
85
+        log("Unable to run because insecure agents are not enabled.  Edit ENABLE_INSECURE_AGENTS in the Huginn .env configuration.")
86
+      end
87
+    end
88
+
89
+    def run_command(path, command)
90
+      result = nil
91
+      errors = nil
92
+      exit_status = nil
93
+
94
+      Dir.chdir(path){
95
+        begin
96
+          stdin, stdout, stderr, wait_thr = Open3.popen3(command)
97
+          exit_status = wait_thr.value.to_i
98
+          result = stdout.gets(nil)
99
+          errors = stderr.gets(nil)
100
+        rescue Exception => e
101
+          errors = e.to_s
102
+        end
103
+      }
104
+
105
+      result = result.to_s.strip
106
+      errors = errors.to_s.strip
107
+
108
+      [result, errors, exit_status]
109
+    end
110
+  end
111
+end

+ 100 - 0
app/models/agents/stubhub_agent.rb

@@ -0,0 +1,100 @@
1
+module Agents
2
+  class StubhubAgent < Agent
3
+    cannot_receive_events!
4
+
5
+    description <<-MD
6
+      This StubHubAgent creates an event for a given StubHub Event. It can be used to track how many tickets are available for the event and the minimum and maximum price. All that is required is that you paste in the url from the actual event, e.g. http://www.stubhub.com/outside-lands-music-festival-tickets/outside-lands-music-festival-3-day-pass-san-francisco-golden-gate-park-polo-fields-8-8-2014-9020701/
7
+    MD
8
+
9
+    event_description <<-MD
10
+      Events looks like this:
11
+        {
12
+          "url": "http://stubhub.com/valid-event-url"
13
+          "name": "Event Name"
14
+          "date": "2014-08-01"
15
+          "max_price": "999.99"
16
+          "min_price": "100.99"
17
+          "total_postings": "50"
18
+          "total_tickets": "150"
19
+          "venue_name": "Venue Name"
20
+        }
21
+    MD
22
+
23
+    default_schedule "every_1d"
24
+
25
+    def working?
26
+      event_created_within?(1) && !recent_error_logs?
27
+    end
28
+
29
+    def default_options
30
+      { 'url' =>  'http://stubhub.com/enter-your-event-here' }
31
+    end
32
+
33
+    def validate_options
34
+      errors.add(:base, 'url is required') unless options['url'].present?
35
+    end
36
+
37
+    def url
38
+      options['url']
39
+    end
40
+
41
+    def check
42
+      create_event :payload => fetch_stubhub_data(url)
43
+    end
44
+
45
+    def fetch_stubhub_data(url)
46
+      StubhubFetcher.call(url)
47
+    end
48
+
49
+    class StubhubFetcher
50
+
51
+      def self.call(url)
52
+        new(url).fields
53
+      end
54
+
55
+      def initialize(url)
56
+        @url = url
57
+      end
58
+
59
+      def event_id
60
+        /(\d*)\/{0,1}\z/.match(url)[1]
61
+      end
62
+
63
+      def base_url
64
+       'http://www.stubhub.com/listingCatalog/select/?q='
65
+      end
66
+
67
+      def build_url
68
+        base_url + "%2B+stubhubDocumentType%3Aevent%0D%0A%2B+event_id%3A#{event_id}%0D%0A&start=0&rows=10&wt=json"
69
+      end
70
+
71
+      def response
72
+        uri = URI(build_url)
73
+        Net::HTTP.get(uri)
74
+      end
75
+
76
+      def parse_response
77
+        JSON.parse(response)
78
+      end
79
+
80
+      def fields
81
+        stubhub_fields = parse_response['response']['docs'][0]
82
+        {
83
+          'url' => url,
84
+          'name' => stubhub_fields['seo_description_en_US'],
85
+          'date' => stubhub_fields['event_date_local'],
86
+          'max_price' => stubhub_fields['maxPrice'].to_s,
87
+          'min_price' => stubhub_fields['minPrice'].to_s,
88
+          'total_postings' => stubhub_fields['totalPostings'].to_s,
89
+          'total_tickets' => stubhub_fields['totalTickets'].to_i.to_s,
90
+          'venue_name' => stubhub_fields['venue_name']
91
+        }
92
+      end
93
+
94
+      private
95
+
96
+      attr_reader :url
97
+
98
+    end
99
+  end
100
+end

+ 36 - 12
app/models/agents/trigger_agent.rb

@@ -11,8 +11,12 @@ module Agents
11 11
 
12 12
       The `type` can be one of #{VALID_COMPARISON_TYPES.map { |t| "`#{t}`" }.to_sentence} and compares with the `value`.
13 13
 
14
+      The `value` can be a single value or an array of values. In the case of an array, if one or more values match then the rule matches. 
15
+
14 16
       All rules must match for the Agent to match.  The resulting Event will have a payload message of `message`.  You can include extractions in the message, for example: `I saw a bar of: <foo.bar>`
15 17
 
18
+      Set `keep_event` to `true` if you'd like to re-emit the incoming event, optionally merged with 'message' when provided.
19
+
16 20
       Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent.
17 21
     MD
18 22
 
@@ -23,15 +27,20 @@ module Agents
23 27
     MD
24 28
 
25 29
     def validate_options
26
-      unless options['expected_receive_period_in_days'].present? && options['message'].present? && options['rules'].present? &&
30
+      unless options['expected_receive_period_in_days'].present? && options['rules'].present? &&
27 31
           options['rules'].all? { |rule| rule['type'].present? && VALID_COMPARISON_TYPES.include?(rule['type']) && rule['value'].present? && rule['path'].present? }
28 32
         errors.add(:base, "expected_receive_period_in_days, message, and rules, with a type, value, and path for every rule, are required")
29 33
       end
34
+
35
+      errors.add(:base, "message is required unless 'keep_event' is 'true'") unless options['message'].present? || keep_event?
36
+
37
+      errors.add(:base, "keep_event, when present, must be 'true' or 'false'") unless options['keep_event'].blank? || %w[true false].include?(options['keep_event'])
30 38
     end
31 39
 
32 40
     def default_options
33 41
       {
34 42
         'expected_receive_period_in_days' => "2",
43
+        'keep_event' => 'false',
35 44
         'rules' => [{
36 45
                       'type' => "regex",
37 46
                       'value' => "foo\\d+bar",
@@ -49,33 +58,48 @@ module Agents
49 58
       incoming_events.each do |event|
50 59
         match = options['rules'].all? do |rule|
51 60
           value_at_path = Utils.value_at(event['payload'], rule['path'])
52
-          case rule['type']
61
+          rule_values = rule['value']
62
+          rule_values = [rule_values] unless rule_values.is_a?(Array)
63
+
64
+          match_found = rule_values.any? do |rule_value|
65
+            case rule['type']
53 66
             when "regex"
54
-              value_at_path.to_s =~ Regexp.new(rule['value'], Regexp::IGNORECASE)
67
+              value_at_path.to_s =~ Regexp.new(rule_value, Regexp::IGNORECASE)
55 68
             when "!regex"
56
-              value_at_path.to_s !~ Regexp.new(rule['value'], Regexp::IGNORECASE)
69
+              value_at_path.to_s !~ Regexp.new(rule_value, Regexp::IGNORECASE)
57 70
             when "field>value"
58
-              value_at_path.to_f > rule['value'].to_f
71
+              value_at_path.to_f > rule_value.to_f
59 72
             when "field>=value"
60
-              value_at_path.to_f >= rule['value'].to_f
73
+              value_at_path.to_f >= rule_value.to_f
61 74
             when "field<value"
62
-              value_at_path.to_f < rule['value'].to_f
75
+              value_at_path.to_f < rule_value.to_f
63 76
             when "field<=value"
64
-              value_at_path.to_f <= rule['value'].to_f
77
+              value_at_path.to_f <= rule_value.to_f
65 78
             when "field==value"
66
-              value_at_path.to_s == rule['value'].to_s
79
+              value_at_path.to_s == rule_value.to_s
67 80
             when "field!=value"
68
-              value_at_path.to_s != rule['value'].to_s
81
+              value_at_path.to_s != rule_value.to_s
69 82
             else
70 83
               raise "Invalid type of #{rule['type']} in TriggerAgent##{id}"
84
+            end
71 85
           end
72 86
         end
73 87
 
74 88
         if match
75
-          create_event :payload => { 'message' => make_message(event[:payload]) } # Maybe this should include the
76
-                                                                                  # original event as well?
89
+          if keep_event?
90
+            payload = event.payload.dup
91
+            payload['message'] = make_message(event[:payload]) if options['message'].present?
92
+          else
93
+            payload = { 'message' => make_message(event[:payload]) }
94
+          end
95
+
96
+          create_event :payload => payload
77 97
         end
78 98
       end
79 99
     end
100
+
101
+    def keep_event?
102
+      options['keep_event'] == 'true'
103
+    end
80 104
   end
81 105
 end

+ 10 - 10
app/models/agents/twilio_agent.rb

@@ -7,17 +7,16 @@ module Agents
7 7
     cannot_create_events!
8 8
 
9 9
     description <<-MD
10
-      The TwilioAgent receives and collects events and sends them via text message or gives you a call when scheduled.
10
+      The TwilioAgent receives and collects events and sends them via text message (up to 160 characters) or gives you a call when scheduled.
11 11
 
12
-      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 Event Formatting Agent if your event does not provide these keys.
12
+      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.
13 13
 
14 14
       Set `receiver_cell` to the number to receive text messages/call and `sender_cell` to the number sending them.
15 15
 
16 16
       `expected_receive_period_in_days` is maximum number of days that you would expect to pass between events being received by this agent.
17 17
 
18
-      If you would like to receive calls, then set `receive_call` to true. `server_url` needs to be 
19
-      filled only if you are making calls. Dont forget to include http/https in `server_url`.
20
-
18
+      If you would like to receive calls, set `receive_call` to `true`. In this case, `server_url` must be set to the URL of your
19
+      Huginn installation (probably "https://#{ENV['DOMAIN']}"), which must be web-accessible.  Be sure to set http/https correctly.
21 20
     MD
22 21
 
23 22
     def default_options
@@ -43,13 +42,14 @@ module Agents
43 42
       @client = Twilio::REST::Client.new options['account_sid'], options['auth_token']
44 43
       memory['pending_calls'] ||= {}
45 44
       incoming_events.each do |event|
46
-        message = (event.payload['message'] || event.payload['text'] || event.payload['sms']).to_s
47
-        if message != ""
45
+        message = (event.payload['message'].presence || event.payload['text'].presence || event.payload['sms'].presence).to_s
46
+        if message.present?
48 47
           if options['receive_call'].to_s == 'true'
49 48
             secret = SecureRandom.hex 3
50 49
             memory['pending_calls'][secret] = message
51 50
             make_call secret
52 51
           end
52
+
53 53
           if options['receive_text'].to_s == 'true'
54 54
             message = message.slice 0..160
55 55
             send_message message
@@ -71,11 +71,11 @@ module Agents
71 71
     def make_call(secret)
72 72
       @client.account.calls.create :from => options['sender_cell'],
73 73
                                    :to => options['receiver_cell'],
74
-                                   :url => post_url(options['server_url'],secret)
74
+                                   :url => post_url(options['server_url'], secret)
75 75
     end
76 76
 
77
-    def post_url(server_url,secret)
78
-      "#{server_url}/users/#{self.user.id}/web_requests/#{self.id}/#{secret}"
77
+    def post_url(server_url, secret)
78
+      "#{server_url}/users/#{user.id}/web_requests/#{id}/#{secret}"
79 79
     end
80 80
 
81 81
     def receive_web_request(params, method, format)

+ 164 - 74
app/models/agents/website_agent.rb

@@ -1,10 +1,10 @@
1 1
 require 'nokogiri'
2
-require 'typhoeus'
2
+require 'faraday'
3
+require 'faraday_middleware'
3 4
 require 'date'
4 5
 
5 6
 module Agents
6 7
   class WebsiteAgent < Agent
7
-    cannot_receive_events!
8 8
 
9 9
     default_schedule "every_12h"
10 10
 
@@ -16,32 +16,42 @@ module Agents
16 16
 
17 17
       Specify a `url` and select a `mode` for when to create Events based on the scraped data, either `all` or `on_change`.
18 18
 
19
+      `url` can be a single url, or an array of urls (for example, for multiple pages with the exact same structure but different content to scrape)
20
+
19 21
       The `type` value can be `xml`, `html`, or `json`.
20 22
 
21 23
       To tell the Agent how to parse the content, specify `extract` as a hash with keys naming the extractions and values of hashes.
22 24
 
23
-      When parsing HTML or XML, these sub-hashes specify how to extract with either a `css` CSS selector or a `xpath` XPath expression and either `'text': true` or `attr` pointing to an attribute name to grab.  An example:
25
+      When parsing HTML or XML, these sub-hashes specify how to extract with either a `css` CSS selector or a `xpath` XPath expression and either `"text": true` or `attr` pointing to an attribute name to grab.  An example:
24 26
 
25
-          'extract': {
26
-            'url': { 'css': "#comic img", 'attr': "src" },
27
-            'title': { 'css': "#comic img", 'attr': "title" },
28
-            'body_text': { 'css': "div.main", 'text': true }
27
+          "extract": {
28
+            "url": { "css": "#comic img", "attr": "src" },
29
+            "title": { "css": "#comic img", "attr": "title" },
30
+            "body_text": { "css": "div.main", "text": true }
29 31
           }
30 32
 
31 33
       When parsing JSON, these sub-hashes specify [JSONPaths](http://goessner.net/articles/JsonPath/) to the values that you care about.  For example:
32 34
 
33
-          'extract': {
34
-            'title': { 'path': "results.data[*].title" },
35
-            'description': { 'path': "results.data[*].description" }
35
+          "extract": {
36
+            "title": { "path": "results.data[*].title" },
37
+            "description": { "path": "results.data[*].description" }
36 38
           }
37 39
 
38 40
       Note that for all of the formats, whatever you extract MUST have the same number of matches for each extractor.  E.g., if you're extracting rows, all extractors must match all rows.  For generating CSS selectors, something like [SelectorGadget](http://selectorgadget.com) may be helpful.
39 41
 
40
-      Can be configured to use HTTP basic auth by including the `basic_auth` parameter with `username:password`.
42
+      Can be configured to use HTTP basic auth by including the `basic_auth` parameter with `"username:password"`, or `["username", "password"]`.
41 43
 
42 44
       Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent.  This is only used to set the "working" status.
43 45
 
44 46
       Set `uniqueness_look_back` to limit the number of events checked for uniqueness (typically for performance).  This defaults to the larger of #{UNIQUENESS_LOOK_BACK} or #{UNIQUENESS_FACTOR}x the number of detected received results.
47
+
48
+      Set `force_encoding` to an encoding name if the website does not return a Content-Type header with a proper charset.
49
+
50
+      Set `user_agent` to a custom User-Agent name if the website does not like the default value ("Faraday v#{Faraday::VERSION}").
51
+
52
+      The `headers` field is optional.  When present, it should be a hash of headers to send with the request.
53
+
54
+      The WebsiteAgent can also scrape based on incoming events. It will scrape the url contained in the `url` key of the incoming event payload.
45 55
     MD
46 56
 
47 57
     event_description do
@@ -85,88 +95,126 @@ module Agents
85 95
       if options['uniqueness_look_back'].present?
86 96
         errors.add(:base, "Invalid uniqueness_look_back format") unless is_positive_integer?(options['uniqueness_look_back'])
87 97
       end
98
+
99
+      if (encoding = options['force_encoding']).present?
100
+        case encoding
101
+        when String
102
+          begin
103
+            Encoding.find(encoding)
104
+          rescue ArgumentError
105
+            errors.add(:base, "Unknown encoding: #{encoding.inspect}")
106
+          end
107
+        else
108
+          errors.add(:base, "force_encoding must be a string")
109
+        end
110
+      end
111
+
112
+      if options['user_agent'].present?
113
+        errors.add(:base, "user_agent must be a string") unless options['user_agent'].is_a?(String)
114
+      end
115
+
116
+      unless headers.is_a?(Hash)
117
+        errors.add(:base, "if provided, headers must be a hash")
118
+      end
119
+
120
+      begin
121
+        basic_auth_credentials()
122
+      rescue => e
123
+        errors.add(:base, e.message)
124
+      end
88 125
     end
89 126
 
90 127
     def check
91
-      hydra = Typhoeus::Hydra.new
92
-      log "Fetching #{options['url']}"
93
-      request_opts = { :followlocation => true }
94
-      request_opts[:userpwd] = options['basic_auth'] if options['basic_auth'].present?
95
-      request = Typhoeus::Request.new(options['url'], request_opts)
96
-
97
-      request.on_failure do |response|
98
-        error "Failed: #{response.inspect}"
99
-      end
128
+      check_url options['url']
129
+    end
100 130
 
101
-      request.on_success do |response|
102
-        doc = parse(response.body)
131
+    def check_url(in_url)
132
+      return unless in_url.present?
103 133
 
104
-        if extract_full_json?
105
-          if store_payload!(previous_payloads(1), doc)
106
-            log "Storing new result for '#{name}': #{doc.inspect}"
107
-            create_event :payload => doc
134
+      Array(in_url).each do |url|
135
+        log "Fetching #{url}"
136
+        response = faraday.get(url)
137
+        if response.success?
138
+          body = response.body
139
+          if (encoding = options['force_encoding']).present?
140
+            body = body.encode(Encoding::UTF_8, encoding)
108 141
           end
109
-        else
110
-          output = {}
111
-          options['extract'].each do |name, extraction_details|
112
-            if extraction_type == "json"
113
-              result = Utils.values_at(doc, extraction_details['path'])
114
-              log "Extracting #{extraction_type} at #{extraction_details['path']}: #{result}"
115
-            else
116
-              case
117
-              when css = extraction_details['css']
118
-                nodes = doc.css(css)
119
-              when xpath = extraction_details['xpath']
120
-                nodes = doc.xpath(xpath)
142
+          doc = parse(body)
143
+
144
+          if extract_full_json?
145
+            if store_payload!(previous_payloads(1), doc)
146
+              log "Storing new result for '#{name}': #{doc.inspect}"
147
+              create_event :payload => doc
148
+            end
149
+          else
150
+            output = {}
151
+            options['extract'].each do |name, extraction_details|
152
+              if extraction_type == "json"
153
+                result = Utils.values_at(doc, extraction_details['path'])
154
+                log "Extracting #{extraction_type} at #{extraction_details['path']}: #{result}"
121 155
               else
122
-                error "'css' or 'xpath' is required for HTML or XML extraction"
123
-                return
124
-              end
125
-              unless Nokogiri::XML::NodeSet === nodes
126
-                error "The result of HTML/XML extraction was not a NodeSet"
127
-                return
128
-              end
129
-              result = nodes.map { |node|
130
-                if extraction_details['attr']
131
-                  node.attr(extraction_details['attr'])
132
-                elsif extraction_details['text']
133
-                  node.text()
156
+                case
157
+                when css = extraction_details['css']
158
+                  nodes = doc.css(css)
159
+                when xpath = extraction_details['xpath']
160
+                  nodes = doc.xpath(xpath)
134 161
                 else
135
-                  error "'attr' or 'text' is required on HTML or XML extraction patterns"
162
+                  error '"css" or "xpath" is required for HTML or XML extraction'
136 163
                   return
137 164
                 end
138
-              }
139
-              log "Extracting #{extraction_type} at #{xpath || css}: #{result}"
165
+                unless Nokogiri::XML::NodeSet === nodes
166
+                  error "The result of HTML/XML extraction was not a NodeSet"
167
+                  return
168
+                end
169
+                result = nodes.map { |node|
170
+                  if extraction_details['attr']
171
+                    node.attr(extraction_details['attr'])
172
+                  elsif extraction_details['text']
173
+                    node.text()
174
+                  else
175
+                    error '"attr" or "text" is required on HTML or XML extraction patterns'
176
+                    return
177
+                  end
178
+                }
179
+                log "Extracting #{extraction_type} at #{xpath || css}: #{result}"
180
+              end
181
+              output[name] = result
140 182
             end
141
-            output[name] = result
142
-          end
143 183
 
144
-          num_unique_lengths = options['extract'].keys.map { |name| output[name].length }.uniq
184
+            num_unique_lengths = options['extract'].keys.map { |name| output[name].length }.uniq
145 185
 
146
-          if num_unique_lengths.length != 1
147
-            error "Got an uneven number of matches for #{options['name']}: #{options['extract'].inspect}"
148
-            return
149
-          end
150
-      
151
-          old_events = previous_payloads num_unique_lengths.first
152
-          num_unique_lengths.first.times do |index|
153
-            result = {}
154
-            options['extract'].keys.each do |name|
155
-              result[name] = output[name][index]
156
-              if name.to_s == 'url'
157
-                result[name] = URI.join(options['url'], result[name]).to_s if (result[name] =~ URI::DEFAULT_PARSER.regexp[:ABS_URI]).nil?
158
-              end
186
+            if num_unique_lengths.length != 1
187
+              error "Got an uneven number of matches for #{options['name']}: #{options['extract'].inspect}"
188
+              return
159 189
             end
160 190
 
161
-            if store_payload!(old_events, result)
162
-              log "Storing new parsed result for '#{name}': #{result.inspect}"
163
-              create_event :payload => result
191
+            old_events = previous_payloads num_unique_lengths.first
192
+            num_unique_lengths.first.times do |index|
193
+              result = {}
194
+              options['extract'].keys.each do |name|
195
+                result[name] = output[name][index]
196
+                if name.to_s == 'url'
197
+                  result[name] = (response.env[:url] + result[name]).to_s
198
+                end
199
+              end
200
+
201
+              if store_payload!(old_events, result)
202
+                log "Storing new parsed result for '#{name}': #{result.inspect}"
203
+                create_event :payload => result
204
+              end
164 205
             end
165 206
           end
207
+        else
208
+          error "Failed: #{response.inspect}"
166 209
         end
167 210
       end
168
-      hydra.queue request
169
-      hydra.run
211
+    end
212
+
213
+    def receive(incoming_events)
214
+      incoming_events.each do |event|
215
+        url_to_scrape = event.payload['url']
216
+        check_url(url_to_scrape) if url_to_scrape =~ /^https?:\/\//i
217
+      end
170 218
     end
171 219
 
172 220
     private
@@ -242,5 +290,47 @@ module Agents
242 290
         false
243 291
       end
244 292
     end
293
+
294
+    def faraday
295
+      @faraday ||= Faraday.new { |builder|
296
+        builder.headers = headers if headers.length > 0
297
+
298
+        if (user_agent = options['user_agent']).present?
299
+          builder.headers[:user_agent] = user_agent
300
+        end
301
+
302
+        builder.use FaradayMiddleware::FollowRedirects
303
+        builder.request :url_encoded
304
+        if userinfo = basic_auth_credentials()
305
+          builder.request :basic_auth, *userinfo
306
+        end
307
+
308
+        case backend = faraday_backend
309
+        when :typhoeus
310
+          require 'typhoeus/adapters/faraday'
311
+        end
312
+        builder.adapter backend
313
+      }
314
+    end
315
+
316
+    def faraday_backend
317
+      ENV.fetch('FARADAY_HTTP_BACKEND', 'typhoeus').to_sym
318
+    end
319
+
320
+    def basic_auth_credentials
321
+      case value = options['basic_auth']
322
+      when nil, ''
323
+        return nil
324
+      when Array
325
+        return value if value.size == 2
326
+      when /:/
327
+        return value.split(/:/, 2)
328
+      end
329
+      raise "bad value for basic_auth: #{value.inspect}"
330
+    end
331
+
332
+    def headers
333
+      options['headers'].presence || {}
334
+    end
245 335
   end
246 336
 end

+ 2 - 2
app/models/user.rb

@@ -23,8 +23,8 @@ class User < ActiveRecord::Base
23 23
   validates_inclusion_of :invitation_code, :on => :create, :in => INVITATION_CODES, :message => "is not valid"
24 24
 
25 25
   has_many :user_credentials, :dependent => :destroy, :inverse_of => :user
26
-  has_many :events, :order => "events.created_at desc", :dependent => :delete_all, :inverse_of => :user
27
-  has_many :agents, :order => "agents.created_at desc", :dependent => :destroy, :inverse_of => :user
26
+  has_many :events, -> { order("events.created_at desc") }, :dependent => :delete_all, :inverse_of => :user
27
+  has_many :agents, -> { order("agents.created_at desc") }, :dependent => :destroy, :inverse_of => :user
28 28
   has_many :logs, :through => :agents, :class_name => "AgentLog"
29 29
 
30 30
   # Allow users to login via either email or username.

+ 1 - 12
app/views/agents/diagram.html.erb

@@ -9,18 +9,7 @@
9 9
       </div>
10 10
 
11 11
       <div class='digraph'>
12
-        <%
13
-           dot_format_string = "digraph foo {"
14
-           @agents.each.with_index do |agent, index|
15
-             dot_format_string += "\"#{agent.name}\";"
16
-             agent.receivers.each do |receiver|
17
-               dot_format_string += "\"#{agent.name}\"->\"#{receiver.name}\";"
18
-             end
19
-           end
20
-           dot_format_string = dot_format_string + "}"
21
-        %>
22
-
23
-        <img src="https://chart.googleapis.com/chart?cht=gv&chl=<%= CGI::escape dot_format_string %>" />
12
+        <%= render_agents_diagram(@agents) %>
24 13
       </div>
25 14
     </div>
26 15
   </div>

+ 1 - 0
app/views/agents/show.html.erb

@@ -18,6 +18,7 @@
18 18
           <% end %>
19 19
           <li><%= link_to '<i class="icon-chevron-left"></i> Back'.html_safe, agents_path %></li>
20 20
           <li><%= link_to '<i class="icon-pencil"></i> Edit'.html_safe, edit_agent_path(@agent) %></li>
21
+          <li><%= link_to '<i class="icon-plus"></i> Clone'.html_safe, new_agent_path(id: @agent) %></li>
21 22
 
22 23
           <% if @agent.can_be_scheduled? || @agent.events.count > 0 %>
23 24
             <li class="dropdown">

+ 1 - 1
app/views/layouts/_messages.html.erb

@@ -1,7 +1,7 @@
1 1
 <% if flash.keys.length > 0 %>
2 2
   <div class="flash">
3 3
     <% flash.each do |name, msg| %>
4
-      <div class="alert alert-<%= name == :notice ? "success" : "error" %>">
4
+      <div class="alert alert-<%= name.to_sym == :notice ? "success" : "error" %>">
5 5
         <a class="close" data-dismiss="alert">&#215;</a>
6 6
         <%= content_tag :div, msg, :id => "flash_#{name}" if msg.is_a?(String) %>
7 7
       </div>

+ 3 - 0
bin/bundle

@@ -0,0 +1,3 @@
1
+#!/usr/bin/env ruby
2
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3
+load Gem.bin_path('bundler', 'bundle')

+ 4 - 0
bin/rails

@@ -0,0 +1,4 @@
1
+#!/usr/bin/env ruby
2
+APP_PATH = File.expand_path('../../config/application',  __FILE__)
3
+require_relative '../config/boot'
4
+require 'rails/commands'

+ 4 - 0
bin/rake

@@ -0,0 +1,4 @@
1
+#!/usr/bin/env ruby
2
+require_relative '../config/boot'
3
+require 'rake'
4
+Rake.application.run

+ 1 - 1
bin/schedule.rb

@@ -64,7 +64,7 @@ class HuginnScheduler
64 64
 
65 65
     # Schedule repeating events.
66 66
 
67
-    %w[2m 5m 10m 30m 1h 2h 5h 12h 1d 2d 7d].each do |schedule|
67
+    %w[1m 2m 5m 10m 30m 1h 2h 5h 12h 1d 2d 7d].each do |schedule|
68 68
       rufus_scheduler.every schedule do
69 69
         run_schedule "every_#{schedule}"
70 70
       end

+ 1 - 13
config/application.rb

@@ -2,12 +2,7 @@ require File.expand_path('../boot', __FILE__)
2 2
 
3 3
 require 'rails/all'
4 4
 
5
-if defined?(Bundler)
6
-  # If you precompile assets before deploying to production, use this line
7
-  Bundler.require(*Rails.groups(:assets => %w(development test)))
8
-  # If you want your assets lazily compiled in production, use this line
9
-  # Bundler.require(:default, :assets, Rails.env)
10
-end
5
+Bundler.require(:default, Rails.env)
11 6
 
12 7
 module Huginn
13 8
   class Application < Rails::Application
@@ -18,10 +13,6 @@ module Huginn
18 13
     # Custom directories with classes and modules you want to be autoloadable.
19 14
     config.autoload_paths += %W(#{config.root}/lib)
20 15
 
21
-    # Only load the plugins named here, in the order given (default is alphabetical).
22
-    # :all can be used as a placeholder for all plugins not explicitly named.
23
-    # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
24
-
25 16
     # Activate observers that should always be running.
26 17
     # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
27 18
 
@@ -56,8 +47,5 @@ module Huginn
56 47
     # Enable the asset pipeline
57 48
     config.assets.enabled = true
58 49
     config.assets.initialize_on_precompile = false
59
-
60
-    # Version of your assets, change this if you want to expire all your assets
61
-    config.assets.version = '1.0'
62 50
   end
63 51
 end

+ 7 - 8
config/environments/development.rb

@@ -8,8 +8,11 @@ Huginn::Application.configure do
8 8
   # since you don't have to restart the web server when you make code changes.
9 9
   config.cache_classes = false
10 10
 
11
-  # Log error messages when you accidentally call methods on nil.
12
-  config.whiny_nils = true
11
+  # Eager load code on boot. This eager loads most of Rails and
12
+  # your application in memory, allowing both threaded web servers
13
+  # and those relying on copy on write to perform better.
14
+  # Rake tasks automatically ignore this option for performance.
15
+  config.eager_load = false
13 16
 
14 17
   # Show full error reports and disable caching
15 18
   config.consider_all_requests_local       = true
@@ -24,12 +27,8 @@ Huginn::Application.configure do
24 27
   # Raise exception on mass assignment protection for Active Record models
25 28
   config.active_record.mass_assignment_sanitizer = :strict
26 29
 
27
-  # Log the query plan for queries taking more than this (works
28
-  # with SQLite, MySQL, and PostgreSQL)
29
-  config.active_record.auto_explain_threshold_in_seconds = 0.5
30
-
31
-  # Do not compress assets
32
-  config.assets.compress = false
30
+  # Raise an error on page load if there are pending migrations.
31
+  config.active_record.migration_error = :page_load
33 32
 
34 33
   # Expands the lines which load the assets
35 34
   config.assets.debug = true

+ 29 - 13
config/environments/production.rb

@@ -4,31 +4,41 @@ Huginn::Application.configure do
4 4
   # Code is not reloaded between requests
5 5
   config.cache_classes = true
6 6
 
7
+  # Eager load code on boot. This eager loads most of Rails and
8
+  # your application in memory, allowing both threaded web servers
9
+  # and those relying on copy on write to perform better.
10
+  # Rake tasks automatically ignore this option for performance.
11
+  config.eager_load = true
12
+
7 13
   # Full error reports are disabled and caching is turned on
8 14
   config.consider_all_requests_local       = false
9 15
   config.action_controller.perform_caching = true
10 16
 
17
+  # Enable Rack::Cache to put a simple HTTP cache in front of your application
18
+  # Add `rack-cache` to your Gemfile before enabling this.
19
+  # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid.
20
+  # config.action_dispatch.rack_cache = true
21
+
11 22
   # Disable Rails's static asset server (Apache or nginx will already do this)
12 23
   config.serve_static_assets = false
13 24
 
14 25
   # Compress JavaScripts and CSS
15
-  config.assets.compress = true
26
+  config.assets.js_compressor  = :uglifier
27
+  config.assets.css_compressor = :sass
16 28
 
17 29
   # Don't fallback to assets pipeline if a precompiled asset is missed
18 30
   config.assets.compile = false
19 31
 
20 32
   # Generate digests for assets URLs
21 33
   config.assets.digest = true
34
+  config.assets.precompile += %w(*.png *.jpg *.jpeg *.gif)
22 35
 
23
-  # Defaults to nil and saved in location specified by config.assets.prefix
24
-  # config.assets.manifest = YOUR_PATH
25
-
26
-  # Specifies the header that your server uses for sending files
36
+  # Specifies the header that your server uses for sending files.
27 37
   # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
28 38
   # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
29 39
 
30 40
   # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
31
-  config.force_ssl = true
41
+  config.force_ssl = ENV['FORCE_SSL'].present? && ENV['FORCE_SSL'] == 'true' ? true : false
32 42
 
33 43
   # See everything in the log (default is :info)
34 44
   # config.log_level = :debug
@@ -50,19 +60,25 @@ Huginn::Application.configure do
50 60
   # Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added)
51 61
   config.assets.precompile += %w( graphing.js user_credentials.js )
52 62
 
53
-  # Enable threaded mode
54
-  # config.threadsafe!
63
+  # Ignore bad email addresses and do not raise email delivery errors.
64
+  # Set this to true and configure the email server for immediate delivery to raise delivery errors.
65
+  # config.action_mailer.raise_delivery_errors = false
55 66
 
56 67
   # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
57
-  # the I18n.default_locale when a translation can not be found)
68
+  # the I18n.default_locale when a translation cannot be found).
58 69
   config.i18n.fallbacks = true
59 70
 
60 71
   # Send deprecation notices to registered listeners
61 72
   config.active_support.deprecation = :notify
62 73
 
63
-  # Log the query plan for queries taking more than this (works
64
-  # with SQLite, MySQL, and PostgreSQL)
65
-  # config.active_record.auto_explain_threshold_in_seconds = 0.5
74
+  # Disable automatic flushing of the log to improve performance.
75
+  # config.autoflush_log = false
76
+
77
+  # Use default logging formatter so that PID and timestamp are not suppressed.
78
+  config.log_formatter = ::Logger::Formatter.new
79
+
80
+  # Do not dump schema after migrations.
81
+  config.active_record.dump_schema_after_migration = false
66 82
 
67 83
   config.action_mailer.default_url_options = { :host => ENV['DOMAIN'] }
68 84
   config.action_mailer.asset_host = ENV['DOMAIN']
@@ -73,4 +89,4 @@ Huginn::Application.configure do
73 89
   config.action_mailer.raise_delivery_errors = true
74 90
   config.action_mailer.delivery_method = :smtp
75 91
   # smtp_settings moved to config/initializers/action_mailer.rb
76
-end
92
+end

+ 0 - 76
config/environments/staging.rb

@@ -1,76 +0,0 @@
1
-Huginn::Application.configure do
2
-  # Settings specified here will take precedence over those in config/application.rb
3
-
4
-  # Code is not reloaded between requests
5
-  config.cache_classes = true
6
-
7
-  # Full error reports are disabled and caching is turned on
8
-  config.consider_all_requests_local       = false
9
-  config.action_controller.perform_caching = true
10
-
11
-  # Disable Rails's static asset server (Apache or nginx will already do this)
12
-  config.serve_static_assets = false
13
-
14
-  # Compress JavaScripts and CSS
15
-  config.assets.compress = true
16
-
17
-  # Don't fallback to assets pipeline if a precompiled asset is missed
18
-  config.assets.compile = false
19
-
20
-  # Generate digests for assets URLs
21
-  config.assets.digest = true
22
-
23
-  # Defaults to nil and saved in location specified by config.assets.prefix
24
-  # config.assets.manifest = YOUR_PATH
25
-
26
-  # Specifies the header that your server uses for sending files
27
-  # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
28
-  # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
29
-
30
-  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
31
-  config.force_ssl = true
32
-
33
-  # See everything in the log (default is :info)
34
-  # config.log_level = :debug
35
-
36
-  # Prepend all log lines with the following tags
37
-  config.log_tags = [ :uuid ] # :subdomain
38
-
39
-  # Use a different logger for distributed setups
40
-  # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
41
-
42
-  # Use a different cache store in production
43
-  # config.cache_store = :mem_cache_store
44
-
45
-  # Enable serving of images, stylesheets, and JavaScripts from an asset server
46
-  if ENV['ASSET_HOST'].present?
47
-    config.action_controller.asset_host = ENV['ASSET_HOST']
48
-  end
49
-
50
-  # Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added)
51
-  config.assets.precompile += %w( graphing.js )
52
-
53
-  # Enable threaded mode
54
-  # config.threadsafe!
55
-
56
-  # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
57
-  # the I18n.default_locale when a translation can not be found)
58
-  config.i18n.fallbacks = true
59
-
60
-  # Send deprecation notices to registered listeners
61
-  config.active_support.deprecation = :notify
62
-
63
-  # Log the query plan for queries taking more than this (works
64
-  # with SQLite, MySQL, and PostgreSQL)
65
-  # config.active_record.auto_explain_threshold_in_seconds = 0.5
66
-
67
-  config.action_mailer.default_url_options = { :host => ENV['DOMAIN'] }
68
-  config.action_mailer.asset_host = ENV['DOMAIN']
69
-  if ENV['ASSET_HOST']
70
-    config.action_mailer.asset_host = ENV['ASSET_HOST']
71
-  end
72
-  config.action_mailer.perform_deliveries = true
73
-  config.action_mailer.raise_delivery_errors = true
74
-  config.action_mailer.delivery_method = :smtp
75
-  # smtp_settings moved to config/initializers/action_mailer.rb
76
-end

+ 6 - 4
config/environments/test.rb

@@ -7,13 +7,15 @@ Huginn::Application.configure do
7 7
   # and recreated between test runs. Don't rely on the data there!
8 8
   config.cache_classes = true
9 9
 
10
+  # Do not eager load code on boot. This avoids loading your whole application
11
+  # just for the purpose of running a single test. If you are using a tool that
12
+  # preloads Rails for running tests, you may have to set it to true.
13
+  config.eager_load = false
14
+
10 15
   # Configure static asset server for tests with Cache-Control for performance
11 16
   config.serve_static_assets = true
12 17
   config.static_cache_control = "public, max-age=3600"
13 18
 
14
-  # Log error messages when you accidentally call methods on nil
15
-  config.whiny_nils = true
16
-
17 19
   # Show full error reports and disable caching
18 20
   config.consider_all_requests_local       = true
19 21
   config.action_controller.perform_caching = false
@@ -22,7 +24,7 @@ Huginn::Application.configure do
22 24
   config.action_dispatch.show_exceptions = false
23 25
 
24 26
   # Disable request forgery protection in test environment
25
-  config.action_controller.allow_forgery_protection    = false
27
+  config.action_controller.allow_forgery_protection = false
26 28
 
27 29
   # Tell Action Mailer not to deliver emails to the real world.
28 30
   # The :test delivery method accumulates sent emails in the

+ 8 - 5
config/initializers/devise.rb

@@ -3,7 +3,8 @@
3 3
 Devise.setup do |config|
4 4
   # ==> Mailer Configuration
5 5
   # Configure the e-mail address which will be shown in Devise::Mailer,
6
-  # note that it will be overwritten if you use your own mailer class with default "from" parameter.
6
+  # note that it will be overwritten if you use your own mailer class
7
+  # with default "from" parameter.
7 8
   config.mailer_sender = "please-change-me-at-config-initializers-devise@example.com"
8 9
 
9 10
   # Configure the class responsible to send e-mails.
@@ -72,6 +73,12 @@ Devise.setup do |config|
72 73
   # passing :skip => :sessions to `devise_for` in your config/routes.rb
73 74
   config.skip_session_storage = [:http_auth]
74 75
 
76
+  # By default, Devise cleans up the CSRF token on authentication to
77
+  # avoid CSRF token fixation attacks. This means that, when using AJAX
78
+  # requests for sign in and sign up, you need to get a new CSRF token
79
+  # from the server. You can disable this option at your own risk.
80
+  # config.clean_up_csrf_token_on_authentication = true
81
+
75 82
   # ==> Configuration for :database_authenticatable
76 83
   # For bcrypt, this is the cost for hashing the password and defaults to 10. If
77 84
   # using other encryptors, it sets how many times you want the password re-encrypted.
@@ -174,10 +181,6 @@ Devise.setup do |config|
174 181
   # REST_AUTH_SITE_KEY to pepper)
175 182
   # config.encryptor = :sha512
176 183
 
177
-  # ==> Configuration for :token_authenticatable
178
-  # Defines name of the authentication token params key
179
-  # config.token_authentication_key = :auth_token
180
-
181 184
   # ==> Scopes configuration
182 185
   # Turn scoped views on. Before rendering "sessions/new", it will first check for
183 186
   # "users/sessions/new". It's turned off by default because it's slower if you

+ 1 - 1
config/initializers/secret_token.rb

@@ -4,4 +4,4 @@
4 4
 # If you change this key, all old signed cookies will become invalid!
5 5
 # Make sure the secret is at least 30 characters and all random,
6 6
 # no regular words or you'll be exposed to dictionary attacks.
7
-Huginn::Application.config.secret_token = ENV['APP_SECRET_TOKEN']
7
+Huginn::Application.config.secret_key_base = ENV['APP_SECRET_TOKEN']

+ 4 - 4
config/routes.rb

@@ -28,18 +28,18 @@ Huginn::Application.routes.draw do
28 28
 
29 29
   resources :user_credentials, :except => :show
30 30
 
31
-  match "/worker_status" => "worker_status#show"
31
+  get "/worker_status" => "worker_status#show"
32 32
 
33 33
   post "/users/:user_id/update_location/:secret" => "user_location_updates#create"
34 34
 
35
-  match "/users/:user_id/web_requests/:agent_id/:secret" => "web_requests#handle_request", :as => :web_requests
35
+  match  "/users/:user_id/web_requests/:agent_id/:secret" => "web_requests#handle_request", :as => :web_requests, :via => [:get, :post, :put, :delete]
36 36
   post "/users/:user_id/webhooks/:agent_id/:secret" => "web_requests#handle_request" # legacy
37 37
 
38 38
 # To enable DelayedJobWeb, see the 'Enable DelayedJobWeb' section of the README.
39
-#  match "/delayed_job" => DelayedJobWeb, :anchor => false
39
+#  get "/delayed_job" => DelayedJobWeb, :anchor => false
40 40
 
41 41
   devise_for :users, :sign_out_via => [ :post, :delete ]
42 42
 
43
-  match "/about" => "home#about"
43
+  get "/about" => "home#about"
44 44
   root :to => "home#index"
45 45
 end

+ 1 - 1
db/seeds.rb

@@ -1,7 +1,7 @@
1 1
 # This file should contain all the record creation needed to seed the database with its default values.
2 2
 # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 3
 
4
-user = User.find_or_initialize_by_email("admin@example.com")
4
+user = User.find_or_initialize_by(:email => "admin@example.com")
5 5
 user.username = "admin"
6 6
 user.password = "password"
7 7
 user.password_confirmation = "password"

+ 3 - 0
deployment/.chef/knife.rb

@@ -0,0 +1,3 @@
1
+cookbook_path ["cookbooks", "site-cookbooks"]
2
+role_path     "roles"
3
+data_bag_path "data_bags"

+ 71 - 0
deployment/Cheffile.lock

@@ -0,0 +1,71 @@
1
+SITE
2
+  remote: http://community.opscode.com/api/v1
3
+  specs:
4
+    apt (2.3.8)
5
+    bluepill (2.3.1)
6
+      rsyslog (>= 0.0.0)
7
+    build-essential (2.0.0)
8
+    chef_handler (1.1.6)
9
+    dmg (2.2.0)
10
+    ohai (1.1.12)
11
+    rsyslog (1.12.2)
12
+    runit (1.5.10)
13
+      build-essential (>= 0.0.0)
14
+      yum (~> 3.0)
15
+      yum-epel (>= 0.0.0)
16
+    windows (1.30.2)
17
+      chef_handler (>= 0.0.0)
18
+    yum (3.2.0)
19
+    yum-epel (0.3.6)
20
+      yum (~> 3.0)
21
+
22
+GIT
23
+  remote: git://github.com/mdxp/nodejs-cookbook.git
24
+  ref: master
25
+  sha: e2415cd8c4e03dccf21d7ef6ca31e1c5c81467ca
26
+  specs:
27
+    nodejs (1.3.0)
28
+      apt (>= 0.0.0)
29
+      build-essential (>= 0.0.0)
30
+      yum-epel (>= 0.0.0)
31
+
32
+GIT
33
+  remote: git://github.com/opscode-cookbooks/git.git
34
+  ref: master
35
+  sha: 76b0f9bb08fdd9e2e201fd70b72298097accdf96
36
+  specs:
37
+    git (4.0.1)
38
+      build-essential (>= 0.0.0)
39
+      dmg (>= 0.0.0)
40
+      runit (>= 1.0)
41
+      windows (>= 0.0.0)
42
+      yum (~> 3.0)
43
+      yum-epel (>= 0.0.0)
44
+
45
+GIT
46
+  remote: git://github.com/opscode-cookbooks/mysql.git
47
+  ref: master
48
+  sha: a2ff53f0ca6deca75aebf6da55ac381194ec7728
49
+  specs:
50
+    mysql (5.1.9)
51
+
52
+GIT
53
+  remote: git://github.com/opscode-cookbooks/nginx.git
54
+  ref: master
55
+  sha: 05b3a613f53a0b05c96f9206c5d67aa420f337fb
56
+  specs:
57
+    nginx (2.6.3)
58
+      apt (~> 2.2)
59
+      bluepill (~> 2.3)
60
+      build-essential (~> 2.0)
61
+      ohai (~> 1.1)
62
+      runit (~> 1.2)
63
+      yum-epel (~> 0.3)
64
+
65
+DEPENDENCIES
66
+  git (>= 0)
67
+  mysql (>= 0)
68
+  nginx (>= 0)
69
+  nodejs (>= 0)
70
+  runit (>= 0)
71
+

+ 28 - 27
deployment/Vagrantfile

@@ -3,37 +3,38 @@
3 3
 
4 4
 Vagrant.configure("2") do |config|
5 5
   config.omnibus.chef_version = :latest
6
-  config.vm.define :vb do |vb|
7
-    vb.vm.box = "precise32"
8
-    vb.vm.box_url = "http://files.vagrantup.com/precise32.box"
9
-    vb.vm.network :forwarded_port, host: 3000, guest: 3000
10 6
 
11
-    vb.vm.provision :chef_solo do |chef|
12
-      chef.roles_path = "roles"
13
-      chef.cookbooks_path = ["cookbooks", "site-cookbooks"]
14
-      chef.add_role("huginn_development")
15
-    end
7
+  config.vm.provision :chef_solo do |chef|
8
+    chef.roles_path = "roles"
9
+    chef.cookbooks_path = ["cookbooks", "site-cookbooks"]
10
+    chef.add_role("huginn_development")
11
+    # chef.add_role("huginn_production")
16 12
   end
17 13
 
18
-  config.vm.define :ec2 do |ec2|
19
-    ec2.vm.box = "dummy"
20
-    ec2.vm.box_url = "https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box"
14
+  config.vm.provider :virtualbox do |vb, override|
15
+    #vb.memory = 1024
16
+    #vb.cpus = 4
17
+    override.vm.box = "hashicorp/precise64"
18
+    override.vm.network :forwarded_port, host: 3000, guest: 3000
19
+  end
20
+
21
+  config.vm.provider :parallels do |prl, override|
22
+    override.vm.box = "parallels/ubuntu-12.04"
23
+  end
24
+
25
+  config.vm.provider :aws do |aws, override|
26
+    aws.ami = ENV['AWS_AMI'] || "ami-828675f5"
27
+    aws.region = ENV['AWS_REGION'] || "eu-west-1"
28
+    aws.instance_type = "t1.micro"
21 29
 
22
-    ec2.vm.provider :aws do |aws, override|
23
-      aws.access_key_id = ""
24
-      aws.secret_access_key = ""
25
-      aws.keypair_name = ""
26
-      aws.region = "us-east-1"
27
-      aws.ami = "ami-d0f89fb9"
30
+    override.vm.box = "dummy"
31
+    override.vm.box_url = "https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box"
32
+    override.ssh.private_key_path = ENV["AWS_SSH_PRIVKEY"]
33
+    override.ssh.username = ENV['AWS_SSH_USER'] || "ubuntu"
28 34
 
29
-      override.ssh.username = "ubuntu"
30
-      override.ssh.private_key_path = ""
31
-    end
32
-    ec2.vm.provision :chef_solo do |chef|
33
-      chef.roles_path = "roles"
34
-      chef.cookbooks_path = ["cookbooks", "site-cookbooks"]
35
-      chef.add_role("huginn_production")
36
-    
37
-    end
35
+    aws.access_key_id = ENV["AWS_ACCESS_KEY_ID"]
36
+    aws.secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
37
+    aws.keypair_name = ENV["AWS_KEYPAIR_NAME"]
38
+    aws.security_groups = [ ENV["AWS_SECURITY_GROUP"] ]
38 39
   end
39 40
 end

+ 1 - 0
deployment/roles/huginn_development.json

@@ -23,6 +23,7 @@
23 23
              "recipe[git]",
24 24
              "recipe[apt]",
25 25
              "recipe[mysql::server]",
26
+             "recipe[mysql::client]",
26 27
              "recipe[nodejs::install_from_binary]",
27 28
              "recipe[huginn_development]"
28 29
            ]

+ 1 - 1
deployment/roles/huginn_production.json

@@ -10,7 +10,7 @@
10 10
 
11 11
 "default_attributes" : {
12 12
   "mysql": {
13
-    "server_root_password": "",
13
+    "server_root_password": "password",
14 14
     "server_repl_password": "",
15 15
     "server_debian_password": ""
16 16
   },

+ 13 - 6
deployment/site-cookbooks/huginn_development/recipes/default.rb

@@ -16,12 +16,19 @@ group "huginn" do
16 16
   action :create
17 17
 end
18 18
 
19
-%w("ruby1.9.1" "ruby1.9.1-dev" "libxslt-dev" "libxml2-dev" "curl").each do |pkg|
19
+%w("ruby1.9.1" "ruby1.9.1-dev" "libxslt-dev" "libxml2-dev" "curl" "libmysqlclient-dev" "rubygems").each do |pkg|
20 20
   package pkg do
21 21
     action :install
22 22
   end
23 23
 end
24 24
 
25
+bash "Setting default ruby version to 1.9" do
26
+  code <<-EOH
27
+    update-alternatives --set ruby /usr/bin/ruby1.9.1
28
+    update-alternatives --set gem /usr/bin/gem1.9.1
29
+  EOH
30
+end
31
+
25 32
 git "/home/huginn/huginn" do
26 33
   repository 'git://github.com/cantino/huginn.git'
27 34
   reference 'master'
@@ -48,10 +55,10 @@ bash "huginn dependencies" do
48 55
     export LANG="en_US.UTF-8"
49 56
     export LC_ALL="en_US.UTF-8"
50 57
     sudo bundle install
51
-    sed s/REPLACE_ME_NOW\!/$(sudo rake secret)/ .env.example > .env
52
-    sudo rake db:create
53
-    sudo rake db:migrate
54
-    sudo rake db:seed
58
+    sed s/REPLACE_ME_NOW\!/$(sudo bundle exec rake secret)/ .env.example > .env
59
+    sudo bundle exec rake db:create
60
+    sudo bundle exec rake db:migrate
61
+    sudo bundle exec rake db:seed
55 62
     EOH
56 63
 end
57 64
 
@@ -59,6 +66,6 @@ bash "huginn has been installed and will start in a minute" do
59 66
   user "huginn"
60 67
   cwd "/home/huginn/huginn"
61 68
   code <<-EOH
62
-    sudo foreman start
69
+    sudo nohup foreman start &
63 70
     EOH
64 71
 end

+ 0 - 58
deployment/site-cookbooks/huginn_production/files/default/Gemfile

@@ -1,58 +0,0 @@
1
-source 'https://rubygems.org'
2
-
3
-gem 'rails'
4
-gem 'rake'
5
-gem 'mysql2'
6
-gem 'devise'
7
-gem 'kaminari'
8
-gem 'bootstrap-kaminari-views'
9
-gem "rufus-scheduler", :require => false
10
-gem 'json', '>= 1.7.7'
11
-gem 'jsonpath'
12
-gem 'twilio-ruby'
13
-
14
-gem 'delayed_job', :git => 'https://github.com/wok/delayed_job' # Until the YAML issues are fixed in master.
15
-gem 'delayed_job_active_record', "~> 0.3.3" # newer was giving a strange MySQL error
16
-gem "daemons"
17
-# gem "delayed_job_web"
18
-group :production do
19
-  gem 'unicorn'
20
-end
21
-gem 'foreman'
22
-gem 'dotenv-rails', :groups => [:development, :test]
23
-
24
-group :assets do
25
-  gem 'sass-rails',   '~> 3.2.3'
26
-  gem 'coffee-rails', '~> 3.2.1'
27
-  gem 'uglifier', '>= 1.0.3'
28
-  gem 'select2-rails'
29
-  gem 'jquery-rails'
30
-end
31
-
32
-gem 'geokit-rails3'
33
-gem 'kramdown'
34
-gem "typhoeus"
35
-gem 'nokogiri'
36
-gem 'wunderground'
37
-
38
-gem "twitter"
39
-gem 'twitter-stream', '>=0.1.16'
40
-gem 'em-http-request'
41
-
42
-platforms :ruby_18 do
43
-  gem 'system_timer'
44
-  gem 'fastercsv'
45
-end
46
-
47
-group :development do
48
-  gem 'pry'
49
-end
50
-
51
-group :development, :test do
52
-  gem 'rspec-rails'
53
-  gem 'rspec'
54
-  gem 'shoulda-matchers'
55
-  gem 'rr'
56
-  gem 'webmock', :require => false
57
-  gem 'rake'
58
-end

+ 4 - 4
deployment/site-cookbooks/huginn_production/files/default/Procfile

@@ -1,4 +1,4 @@
1
-web: sudo bundle exec unicorn_rails -c config/unicorn.rb
2
-schedule: sudo bundle exec rails runner bin/schedule.rb
3
-twitter: sudo bundle exec rails runner bin/twitter_stream.rb
4
-dj: sudo bundle exec script/delayed_job run
1
+web: sudo bundle exec unicorn_rails -c config/unicorn.rb -E production
2
+schedule: sudo RAILS_ENV=production bundle exec rails runner bin/schedule.rb
3
+twitter: sudo RAILS_ENV=production bundle exec rails runner bin/twitter_stream.rb
4
+dj: sudo RAILS_ENV=production bundle exec script/delayed_job run

+ 68 - 7
deployment/site-cookbooks/huginn_production/files/default/env.example

@@ -3,18 +3,22 @@
3 3
 # Replace the following with the output from "rake secret"
4 4
 APP_SECRET_TOKEN=REPLACE_ME_NOW!
5 5
 
6
-# This is the domain where your Huginn instance will be running.  The default should work
7
-# for development, but it needs to be changed when you deploy to a production environment.
6
+# This is the domain where your Huginn instance will be running. The default should work
7
+# for development, but it needs to be changed to your Huginn domain when you deploy to a
8
+# production environment (e.g., yourdomain.com, possibly including a port).
8 9
 #DOMAIN=localhost:3000
9 10
 
10
-# Database Setup
11
+############################
12
+#      Database Setup      #
13
+############################
14
+
11 15
 DATABASE_ADAPTER=mysql2
12 16
 DATABASE_ENCODING=utf8
13 17
 DATABASE_RECONNECT=true
14 18
 DATABASE_NAME=huginn_production
15 19
 DATABASE_POOL=5
16 20
 DATABASE_USERNAME=root
17
-DATABASE_PASSWORD=
21
+DATABASE_PASSWORD=password
18 22
 #DATABASE_HOST=your-domain-here.com
19 23
 #DATABASE_PORT=3306
20 24
 #DATABASE_SOCKET=/tmp/mysql.sock
@@ -24,8 +28,27 @@ DATABASE_PASSWORD=
24 28
 # Configure Rails environment.  This should only be needed in production and may cause errors in development.
25 29
 RAILS_ENV=production
26 30
 
31
+# Should Rails force all requests to use SSL?
32
+FORCE_SSL=false
33
+
34
+############################
35
+#     Allowing Signups     #
36
+############################
37
+
38
+# This invitation code will be required for users to signup with your Huginn installation.
39
+# You can see its use in user.rb.  PLEASE CHANGE THIS!
40
+INVITATION_CODE=try-huginn
41
+
42
+#############################
43
+#    Email Configuration    #
44
+#############################
45
+
27 46
 # Outgoing email settings.  To use Gmail or Google Apps, put your Google Apps domain or gmail.com
28 47
 # as the SMTP_DOMAIN and your Gmail username and password as the SMTP_USER_NAME and SMTP_PASSWORD.
48
+# 
49
+# PLEASE NOTE: In order to enable emails locally (e.g., when not in the production Rails environment), 
50
+# you must also change config.action_mailer.perform_deliveries in config/environments/development.rb.
51
+
29 52
 SMTP_DOMAIN=your-domain-here.com
30 53
 SMTP_USER_NAME=you@gmail.com
31 54
 SMTP_PASSWORD=somepassword
@@ -37,6 +60,44 @@ SMTP_ENABLE_STARTTLS_AUTO=true
37 60
 # The address from which system emails will appear to be sent.
38 61
 EMAIL_FROM_ADDRESS=from_address@gmail.com
39 62
 
40
-# This invitation code will be required for users to signup with your Huginn installation.
41
-# You can see its use in user.rb.
42
-INVITATION_CODE=try-huginn
63
+###########################
64
+#      Agent Logging      #
65
+###########################
66
+
67
+# Number of lines of log messages to keep per Agent
68
+AGENT_LOG_LENGTH=200
69
+
70
+#############################
71
+#  AWS and Mechanical Turk  #
72
+#############################
73
+
74
+# AWS Credentials for MTurk
75
+AWS_ACCESS_KEY_ID="your aws access key id"
76
+AWS_ACCESS_KEY="your aws access key"
77
+
78
+# Set AWS_SANDBOX to true if you're developing Huginn code.
79
+AWS_SANDBOX=false
80
+
81
+########################
82
+#   Various Settings   #
83
+########################
84
+
85
+# Specify the HTTP backend library for Faraday, used in WebsiteAgent.
86
+# You can change this depending on the performance and stability you
87
+# need for your service.  Any choice other than "typhoeus",
88
+# "net_http", or "em_http" should require you to bundle a corresponding
89
+# gem via Gemfile.
90
+FARADAY_HTTP_BACKEND=typhoeus
91
+
92
+# Allow JSONPath eval expresions. i.e., $..price[?(@ < 20)]
93
+# You should not allow this on a shared Huginn box because it is not secure.
94
+ALLOW_JSONPATH_EVAL=false
95
+
96
+# Enable this setting to allow insecure Agents like the ShellCommandAgent.  Only do this
97
+# when you trust everyone using your Huginn installation.
98
+ENABLE_INSECURE_AGENTS=false
99
+
100
+# Use Graphviz for generating diagrams instead of using Google Chart
101
+# Tools.  Specify a dot(1) command path built with SVG support
102
+# enabled.
103
+#USE_GRAPHVIZ_DOT=dot

+ 5 - 6
deployment/site-cookbooks/huginn_production/files/default/nginx.conf

@@ -1,15 +1,18 @@
1 1
 #worker_process 2;
2 2
 user huginn huginn;
3 3
 
4
-events { 
4
+events {
5 5
   worker_connections 1024;
6 6
   accept_mutex on;
7 7
 }
8 8
 
9 9
 http {
10
+  types_hash_max_size 2048;
11
+  include    mime.types;
12
+
10 13
   upstream huginn_server {
11 14
     server unix:/home/huginn/shared/tmp/sockets/unicorn.sock;
12
-}
15
+  }
13 16
 
14 17
   server {
15 18
     listen 80;
@@ -23,13 +26,9 @@ http {
23 26
     }
24 27
     location @app {
25 28
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
26
-
27 29
       proxy_set_header X-Forwarded-Proto $scheme;
28
-
29 30
       proxy_set_header Host $http_host;
30
-
31 31
       proxy_redirect off;
32
-
33 32
       proxy_pass http://huginn_server;
34 33
     }
35 34
 }

+ 4 - 2
deployment/site-cookbooks/huginn_production/files/default/unicorn.rb

@@ -17,7 +17,8 @@ stdout_path "log/unicorn_err.log"
17 17
 pid '/home/huginn/shared/tmp/pids/unicorn.pid'
18 18
 
19 19
 before_fork do |server, worker|
20
-  ActiveRecord::Base.connection.disconnect!
20
+  defined?(ActiveRecord::Base) and
21
+    ActiveRecord::Base.connection.disconnect!
21 22
   old_pid = "#{server.config[:pid]}.oldbin"
22 23
   if File.exists?(old_pid) && server.pid != old_pid
23 24
     begin
@@ -29,5 +30,6 @@ before_fork do |server, worker|
29 30
 end
30 31
 
31 32
 after_fork do |server, worker|
32
-  ActiveRecord::Base.establish_connection
33
+  defined?(ActiveRecord::Base) and
34
+    ActiveRecord::Base.establish_connection
33 35
 end

+ 18 - 9
deployment/site-cookbooks/huginn_production/recipes/default.rb

@@ -14,10 +14,17 @@ group "huginn" do
14 14
   members ["huginn"]
15 15
 end
16 16
 
17
-%w("ruby1.9.1" "ruby1.9.1-dev" "libxslt-dev" "libxml2-dev" "curl" "libshadow-ruby1.8").each do |pkg|
17
+%w("ruby1.9.1" "ruby1.9.1-dev" "libxslt-dev" "libxml2-dev" "curl" "libshadow-ruby1.8" "libmysqlclient-dev" "libffi-dev" "libssl-dev" "rubygems").each do |pkg|
18 18
   package("#{pkg}")
19 19
 end
20 20
 
21
+bash "Setting default ruby version to 1.9" do
22
+  code <<-EOH
23
+    update-alternatives --set ruby /usr/bin/ruby1.9.1
24
+    update-alternatives --set gem /usr/bin/gem1.9.1
25
+  EOH
26
+end
27
+
21 28
 gem_package("rake")
22 29
 gem_package("bundle")
23 30
 
@@ -36,6 +43,7 @@ end
36 43
 
37 44
 deploy "/home/huginn" do
38 45
   repo "https://github.com/cantino/huginn.git"
46
+  branch "master"
39 47
   user "huginn"
40 48
   group "huginn"
41 49
   environment "RAILS_ENV" => "production"
@@ -56,7 +64,7 @@ deploy "/home/huginn" do
56 64
     end
57 65
     directory("/home/huginn/shared/tmp/pids")
58 66
     directory("/home/huginn/shared/tmp/sockets")
59
-    %w(Procfile unicorn.rb Gemfile nginx.conf).each do |file|
67
+    %w(Procfile unicorn.rb nginx.conf).each do |file|
60 68
       cookbook_file "/home/huginn/shared/config/#{file}" do
61 69
       owner "huginn"
62 70
       action :create_if_missing
@@ -77,16 +85,17 @@ deploy "/home/huginn" do
77 85
       code <<-EOH
78 86
       export LANG="en_US.UTF-8"
79 87
       export LC_ALL="en_US.UTF-8"
80
-      ln -nfs /home/huginn/shared/config/Gemfile ./Gemfile
81 88
       ln -nfs /home/huginn/shared/config/Procfile ./Procfile
82 89
       ln -nfs /home/huginn/shared/config/.env ./.env
83 90
       ln -nfs /home/huginn/shared/config/unicorn.rb ./config/unicorn.rb
84
-      sudo cp /home/huginn/shared/config/nginx.conf /etc/nginx/ 
85
-      sudo bundle install
86
-      sed -i s/REPLACE_ME_NOW\!/$(sudo rake secret)/ .env
87
-      sudo rake db:create
88
-      sudo rake db:migrate
89
-      sudo rake db:seed
91
+      sudo cp /home/huginn/shared/config/nginx.conf /etc/nginx/
92
+      echo 'gem "unicorn", :group => :production' >> Gemfile
93
+      sudo bundle install --without=development --without=test
94
+      sed -i s/REPLACE_ME_NOW\!/$(sudo bundle exec rake secret)/ /home/huginn/shared/config/.env
95
+      sudo RAILS_ENV=production bundle exec rake db:create
96
+      sudo RAILS_ENV=production bundle exec rake db:migrate
97
+      sudo RAILS_ENV=production bundle exec rake db:seed
98
+      sudo RAILS_ENV=production bundle exec rake assets:precompile
90 99
       sudo foreman export upstart /etc/init -a huginn -u huginn -l log
91 100
       sudo start huginn
92 101
       EOH

+ 0 - 6
deployment/solo.rb

@@ -1,6 +0,0 @@
1
-file_cache_path           "/tmp/chef-solo"
2
-data_bag_path             "/tmp/chef-solo/data_bags"
3
-encrypted_data_bag_secret "/tmp/chef-solo/data_bag_key"
4
-cookbook_path             [ "/tmp/chef-solo/site-cookbooks",
5
-                            "/tmp/chef-solo/cookbooks" ]
6
-role_path                 "/tmp/chef-solo/roles"

+ 2 - 2
lib/rdbms_functions.rb

@@ -1,10 +1,10 @@
1 1
 module RDBMSFunctions
2 2
   def rdbms_date_add(source, unit, amount)
3
-    adapter_type = connection.adapter_name.downcase.to_sym
3
+    adapter_type = ActiveRecord::Base.connection.adapter_name.downcase.to_sym
4 4
     case adapter_type
5 5
       when :mysql, :mysql2
6 6
         "DATE_ADD(`#{source}`, INTERVAL #{amount} #{unit})"
7
-      when :postgresql    
7
+      when :postgresql
8 8
         "(#{source} + INTERVAL '#{amount} #{unit}')"
9 9
       else
10 10
         raise NotImplementedError, "Unknown adapter type '#{adapter_type}'"

+ 2 - 2
lib/utils.rb

@@ -56,7 +56,7 @@ module Utils
56 56
       escape = false
57 57
     end
58 58
 
59
-    result = JsonPath.new(path, :allow_eval => false).on(data.is_a?(String) ? data : data.to_json)
59
+    result = JsonPath.new(path, :allow_eval => ENV['ALLOW_JSONPATH_EVAL'] == "true").on(data.is_a?(String) ? data : data.to_json)
60 60
     if escape
61 61
       result.map {|r| CGI::escape r }
62 62
     else
@@ -79,4 +79,4 @@ module Utils
79 79
   def self.pretty_jsonify(thing)
80 80
     JSON.pretty_generate(thing).gsub('</', '<\/')
81 81
   end
82
-end
82
+end

+ 16 - 0
spec/controllers/agents_controller_spec.rb

@@ -46,6 +46,22 @@ describe AgentsController do
46 46
     end
47 47
   end
48 48
 
49
+  describe "GET new with :id" do
50
+    it "opens a clone of a given Agent" do
51
+      sign_in users(:bob)
52
+      get :new, :id => agents(:bob_website_agent).to_param
53
+      assigns(:agent).attributes.should eq(users(:bob).agents.build_clone(agents(:bob_website_agent)).attributes)
54
+    end
55
+
56
+    it "only allows the current user to clone his own Agent" do
57
+      sign_in users(:bob)
58
+
59
+      lambda {
60
+        get :new, :id => agents(:jane_website_agent).to_param
61
+      }.should raise_error(ActiveRecord::RecordNotFound)
62
+    end
63
+  end
64
+
49 65
   describe "GET edit" do
50 66
     it "only shows Agents for the current user" do
51 67
       sign_in users(:bob)

+ 21 - 0
spec/data_fixtures/basecamp.json

@@ -0,0 +1,21 @@
1
+[
2
+  {
3
+    "creator": {
4
+      "fullsize_avatar_url": "https://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/original.gif?r=3",
5
+      "avatar_url": "http://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/avatar.gif?r=3",
6
+      "name": "Dominik Sander",
7
+      "id": 123456
8
+    },
9
+    "attachments": [],
10
+    "raw_excerpt": "test test",
11
+    "excerpt": "test test",
12
+    "id": 6454342343,
13
+    "created_at": "2014-04-17T10:25:31.000+02:00",
14
+    "updated_at": "2014-04-17T10:25:31.000+02:00",
15
+    "summary": "commented on whaat",
16
+    "action": "commented on",
17
+    "target": "whaat",
18
+    "url": "https://basecamp.com/12456/api/v1/projects/5476464-explore-basecamp/messages/24598238-whaat.json",
19
+    "html_url": "https://basecamp.com/12456/projects/5476464-explore-basecamp/messages/24598238-whaat#comment_150756301"
20
+  }
21
+]

+ 17 - 0
spec/data_fixtures/stubhub_data.json

@@ -0,0 +1,17 @@
1
+{
2
+  "response":{
3
+    "docs":[
4
+      {
5
+      "url": "http://www.stubhub.com/event/name-1-1-2014-12345",
6
+      "seo_description_en_US": "name",
7
+      "event_date_local": "2014-01-01",
8
+      "maxPrice": "100",
9
+      "minPrice": "50",
10
+      "totalPostings": "100",
11
+      "totalTickets": "200",
12
+      "venue_name": "Venue Name"
13
+    }
14
+    ]
15
+  }
16
+}
17
+

+ 48 - 0
spec/helpers/dot_helper_spec.rb

@@ -0,0 +1,48 @@
1
+require 'spec_helper'
2
+
3
+describe DotHelper do
4
+  describe "#dot_id" do
5
+    it "properly escapes double quotaion and backslash" do
6
+      dot_id('hello\\"').should == '"hello\\\\\\""'
7
+    end
8
+  end
9
+
10
+  describe "with example Agents" do
11
+    class Agents::DotFoo < Agent
12
+      default_schedule "2pm"
13
+
14
+      def check
15
+        create_event :payload => {}
16
+      end
17
+    end
18
+
19
+    class Agents::DotBar < Agent
20
+      cannot_be_scheduled!
21
+
22
+      def check
23
+        create_event :payload => {}
24
+      end
25
+    end
26
+
27
+    before do
28
+      stub(Agents::DotFoo).valid_type?("Agents::DotFoo") { true }
29
+      stub(Agents::DotBar).valid_type?("Agents::DotBar") { true }
30
+    end
31
+
32
+    describe "#agents_dot" do
33
+      it "generates a DOT script" do
34
+        @foo = Agents::DotFoo.new(:name => "foo")
35
+        @foo.user = users(:bob)
36
+        @foo.save!
37
+
38
+        @bar = Agents::DotBar.new(:name => "bar")
39
+        @bar.user = users(:bob)
40
+        @bar.sources << @foo
41
+        @bar.save!
42
+
43
+        agents_dot([@foo, @bar]).should == 'digraph foo {"foo";"foo"->"bar";"bar";}'
44
+        agents_dot([@foo, @bar], true).should == 'digraph foo {"foo"[URL="/agents/%d"];"foo"->"bar";"bar"[URL="/agents/%d"];}' % [@foo.id, @bar.id]
45
+      end
46
+    end
47
+  end
48
+end

+ 1 - 1
spec/lib/utils_spec.rb

@@ -97,7 +97,7 @@ describe Utils do
97 97
     it "escapes </script> tags in the output JSON" do
98 98
       cleaned_json = Utils.jsonify(:foo => "bar", :xss => "</script><script>alert('oh no!')</script>")
99 99
       cleaned_json.should_not include("</script>")
100
-      cleaned_json.should include("<\\/script>")
100
+      cleaned_json.should include('\\u003c/script\\u003e')
101 101
     end
102 102
 
103 103
     it "html_safes the output unless :skip_safe is passed in" do

+ 48 - 26
spec/models/agent_spec.rb

@@ -1,6 +1,9 @@
1 1
 require 'spec_helper'
2
+require 'models/concerns/working_helpers'
2 3
 
3 4
 describe Agent do
5
+  it_behaves_like WorkingHelpers
6
+
4 7
   describe ".run_schedule" do
5 8
     before do
6 9
       Agents::WeatherAgent.count.should > 0
@@ -514,6 +517,51 @@ describe Agent do
514 517
         end
515 518
       end
516 519
     end
520
+
521
+    describe "Agent.build_clone" do
522
+      before do
523
+        Event.delete_all
524
+        @sender = Agents::SomethingSource.new(
525
+          name: 'Agent (2)',
526
+          options: { foo: 'bar2' },
527
+          schedule: '5pm')
528
+        @sender.user = users(:bob)
529
+        @sender.save!
530
+        @sender.create_event :payload => {}
531
+        @sender.create_event :payload => {}
532
+        @sender.events.count.should == 2
533
+
534
+        @receiver = Agents::CannotBeScheduled.new(
535
+          name: 'Agent',
536
+          options: { foo: 'bar3' },
537
+          keep_events_for: 3,
538
+          propagate_immediately: true)
539
+        @receiver.user = users(:bob)
540
+        @receiver.sources << @sender
541
+        @receiver.memory[:test] = 1
542
+        @receiver.save!
543
+      end
544
+
545
+      it "should create a clone of a given agent for editing" do
546
+        sender_clone = users(:bob).agents.build_clone(@sender)
547
+
548
+        sender_clone.attributes.should == Agent.new.attributes.
549
+          update(@sender.slice(:user_id, :type,
550
+            :options, :schedule, :keep_events_for, :propagate_immediately)).
551
+          update('name' => 'Agent (2) (2)', 'options' => { 'foo' => 'bar2' })
552
+
553
+        sender_clone.source_ids.should == []
554
+
555
+        receiver_clone = users(:bob).agents.build_clone(@receiver)
556
+
557
+        receiver_clone.attributes.should == Agent.new.attributes.
558
+          update(@receiver.slice(:user_id, :type,
559
+            :options, :schedule, :keep_events_for, :propagate_immediately)).
560
+          update('name' => 'Agent (3)', 'options' => { 'foo' => 'bar3' })
561
+
562
+        receiver_clone.source_ids.should == [@sender.id]
563
+      end
564
+    end
517 565
   end
518 566
 
519 567
   describe ".trigger_web_request" do
@@ -565,32 +613,6 @@ describe Agent do
565 613
     end
566 614
   end
567 615
 
568
-  describe "recent_error_logs?" do
569
-    it "returns true if last_error_log_at is near last_event_at" do
570
-      agent = Agent.new
571
-
572
-      agent.last_error_log_at = 10.minutes.ago
573
-      agent.last_event_at = 10.minutes.ago
574
-      agent.recent_error_logs?.should be_true
575
-
576
-      agent.last_error_log_at = 11.minutes.ago
577
-      agent.last_event_at = 10.minutes.ago
578
-      agent.recent_error_logs?.should be_true
579
-
580
-      agent.last_error_log_at = 5.minutes.ago
581
-      agent.last_event_at = 10.minutes.ago
582
-      agent.recent_error_logs?.should be_true
583
-
584
-      agent.last_error_log_at = 15.minutes.ago
585
-      agent.last_event_at = 10.minutes.ago
586
-      agent.recent_error_logs?.should be_false
587
-
588
-      agent.last_error_log_at = 2.days.ago
589
-      agent.last_event_at = 10.minutes.ago
590
-      agent.recent_error_logs?.should be_false
591
-    end
592
-  end
593
-
594 616
   describe "scopes" do
595 617
     describe "of_type" do
596 618
       it "should accept classes" do

+ 85 - 0
spec/models/agents/basecamp_agent_spec.rb

@@ -0,0 +1,85 @@
1
+require 'spec_helper'
2
+
3
+describe Agents::BasecampAgent do
4
+  before(:each) do
5
+    stub_request(:get, /json$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"})
6
+    stub_request(:get, /Z$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"})
7
+    @valid_params = {
8
+                      :username   => "user",
9
+                      :password   => "pass",
10
+                      :user_id    => 12345,
11
+                      :project_id => 6789,
12
+                    }
13
+
14
+    @checker = Agents::BasecampAgent.new(:name => "somename", :options => @valid_params)
15
+    @checker.user = users(:jane)
16
+    @checker.save!
17
+  end
18
+
19
+  describe "validating" do
20
+    before do
21
+      @checker.should be_valid
22
+    end
23
+
24
+    it "should require the basecamp username" do
25
+      @checker.options['username'] = nil
26
+      @checker.should_not be_valid
27
+    end
28
+
29
+    it "should require the basecamp password" do
30
+      @checker.options['password'] = nil
31
+      @checker.should_not be_valid
32
+    end
33
+
34
+    it "should require the basecamp user_id" do
35
+      @checker.options['user_id'] = nil
36
+      @checker.should_not be_valid
37
+    end
38
+
39
+    it "should require the basecamp project_id" do
40
+      @checker.options['project_id'] = nil
41
+      @checker.should_not be_valid
42
+    end
43
+
44
+  end
45
+
46
+  describe "helpers" do
47
+    it "should generate a correct request options hash" do
48
+      @checker.send(:request_options).should == {:basic_auth=>{:username=>"user", :password=>"pass"}, :headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)"}}
49
+    end
50
+
51
+    it "should generate the currect request url" do
52
+      @checker.send(:request_url).should == "https://basecamp.com/12345/api/v1/projects/6789/events.json"
53
+    end
54
+
55
+
56
+    it "should not provide the since attribute on first run" do
57
+      @checker.send(:query_parameters).should == {}
58
+    end
59
+
60
+    it "should provide the since attribute after the first run" do
61
+      time = (Time.now-1.minute).iso8601
62
+      @checker.memory[:last_run] = time
63
+      @checker.save
64
+      @checker.reload.send(:query_parameters).should == {:query => {:since => time}}
65
+    end
66
+  end
67
+  describe "#check" do
68
+    it "should not emit events on its first run" do
69
+      expect { @checker.check }.to change { Event.count }.by(0)
70
+    end
71
+    it "should check that initial run creates an event" do
72
+      @checker.last_check_at = Time.now - 1.minute
73
+      expect { @checker.check }.to change { Event.count }.by(1)
74
+    end
75
+  end
76
+
77
+  describe "#working?" do
78
+    it "it is working when at least one event was emited" do
79
+      @checker.should_not be_working
80
+      @checker.last_check_at = Time.now - 1.minute
81
+      @checker.check
82
+      @checker.reload.should be_working
83
+    end
84
+  end
85
+end

+ 41 - 4
spec/models/agents/event_formatting_agent_spec.rb

@@ -7,9 +7,16 @@ describe Agents::EventFormattingAgent do
7 7
         :options => {
8 8
             :instructions => {
9 9
                 :message => "Received <$.content.text.*> from <$.content.name> .",
10
-                :subject => "Weather looks like <$.conditions>"
10
+                :subject => "Weather looks like <$.conditions> according to the forecast at <$.pretty_date.time>"
11 11
             },
12 12
             :mode => "clean",
13
+            :matchers => [
14
+                {
15
+                    :path => "$.date.pretty",
16
+                    :regexp => "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)",
17
+                    :to => "pretty_date",
18
+                },
19
+            ],
13 20
             :skip_agent => "false",
14 21
             :skip_created_at => "false"
15 22
         }
@@ -24,7 +31,11 @@ describe Agents::EventFormattingAgent do
24 31
     @event.payload = {
25 32
         :content => {
26 33
             :text => "Some Lorem Ipsum",
27
-            :name => "somevalue"
34
+            :name => "somevalue",
35
+        },
36
+        :date => {
37
+            :epoch => "1357959600",
38
+            :pretty => "10:00 PM EST on January 11, 2013"
28 39
         },
29 40
         :conditions => "someothervalue"
30 41
     }
@@ -61,7 +72,11 @@ describe Agents::EventFormattingAgent do
61 72
     it "should handle JSONPaths in instructions" do
62 73
       @checker.receive([@event])
63 74
       Event.last.payload[:message].should == "Received Some Lorem Ipsum from somevalue ."
64
-      Event.last.payload[:subject].should == "Weather looks like someothervalue"
75
+    end
76
+
77
+    it "should handle matchers and JSONPaths in instructions" do
78
+      @checker.receive([@event])
79
+      Event.last.payload[:subject].should == "Weather looks like someothervalue according to the forecast at 10:00 PM EST"
65 80
     end
66 81
 
67 82
     it "should allow escaping" do
@@ -110,6 +125,28 @@ describe Agents::EventFormattingAgent do
110 125
       @checker.should_not be_valid
111 126
     end
112 127
 
128
+    it "should validate type of matchers" do
129
+      @checker.options[:matchers] = ""
130
+      @checker.should_not be_valid
131
+      @checker.options[:matchers] = {}
132
+      @checker.should_not be_valid
133
+    end
134
+
135
+    it "should validate the contents of matchers" do
136
+      @checker.options[:matchers] = [
137
+        {}
138
+      ]
139
+      @checker.should_not be_valid
140
+      @checker.options[:matchers] = [
141
+        { :regexp => "(not closed", :path => "text" }
142
+      ]
143
+      @checker.should_not be_valid
144
+      @checker.options[:matchers] = [
145
+        { :regexp => "(closed)", :path => "text", :to => "foo" }
146
+      ]
147
+      @checker.should be_valid
148
+    end
149
+
113 150
     it "should validate presence of mode" do
114 151
       @checker.options[:mode] = ""
115 152
       @checker.should_not be_valid
@@ -125,4 +162,4 @@ describe Agents::EventFormattingAgent do
125 162
       @checker.should_not be_valid
126 163
     end
127 164
   end
128
-end
165
+end

+ 83 - 0
spec/models/agents/hipchat_agent_spec.rb

@@ -0,0 +1,83 @@
1
+require 'spec_helper'
2
+require 'models/concerns/json_path_options_overwritable'
3
+
4
+describe Agents::HipchatAgent do
5
+  it_behaves_like JsonPathOptionsOverwritable
6
+
7
+  before(:each) do
8
+    @valid_params = {
9
+                      'auth_token' => 'token',
10
+                      'room_name' => 'test',
11
+                      'room_name_path' => '',
12
+                      'username' => "Huginn",
13
+                      'username_path' => '$.username',
14
+                      'message' => "Hello from Huginn!",
15
+                      'message_path' => '$.message',
16
+                      'notify' => false,
17
+                      'notify_path' => '',
18
+                      'color' => 'yellow',
19
+                      'color_path' => '',
20
+                    }
21
+
22
+    @checker = Agents::HipchatAgent.new(:name => "somename", :options => @valid_params)
23
+    @checker.user = users(:jane)
24
+    @checker.save!
25
+
26
+    @event = Event.new
27
+    @event.agent = agents(:bob_weather_agent)
28
+    @event.payload = { :room_name => 'test room', :message => 'Looks like its going to rain', username: "Huggin user"}
29
+    @event.save!
30
+  end
31
+
32
+  describe "validating" do
33
+    before do
34
+      @checker.should be_valid
35
+    end
36
+
37
+    it "should require the basecamp username" do
38
+      @checker.options['auth_token'] = nil
39
+      @checker.should_not be_valid
40
+    end
41
+
42
+    it "should require the basecamp password" do
43
+      @checker.options['room_name'] = nil
44
+      @checker.should_not be_valid
45
+    end
46
+
47
+    it "should require the basecamp user_id" do
48
+      @checker.options['room_name'] = nil
49
+      @checker.options['room_name_path'] = 'jsonpath'
50
+      @checker.should be_valid
51
+    end
52
+
53
+  end
54
+
55
+  describe "#receive" do
56
+    it "send a message to the hipchat" do
57
+      any_instance_of(HipChat::Room) do |obj|
58
+        mock(obj).send(@event.payload[:username], @event.payload[:message], {:notify => 0, :color => 'yellow'})
59
+      end
60
+      @checker.receive([@event])
61
+    end
62
+  end
63
+
64
+  describe "#working?" do
65
+    it "should not be working until the first event was received" do
66
+      @checker.should_not be_working
67
+      @checker.last_receive_at = Time.now
68
+      @checker.should be_working
69
+    end
70
+
71
+    it "should not be working when the last error occured after the last received event" do
72
+      @checker.last_receive_at = Time.now - 1.minute
73
+      @checker.last_error_log_at = Time.now
74
+      @checker.should_not be_working
75
+    end
76
+
77
+    it "should be working when the last received event occured after the last error" do
78
+      @checker.last_receive_at = Time.now
79
+      @checker.last_error_log_at = Time.now - 1.minute
80
+      @checker.should be_working
81
+    end
82
+  end
83
+end

+ 127 - 14
spec/models/agents/post_agent_spec.rb

@@ -5,8 +5,11 @@ describe Agents::PostAgent do
5 5
     @valid_params = {
6 6
       :name => "somename",
7 7
       :options => {
8
-        :post_url => "http://www.example.com",
9
-        :expected_receive_period_in_days => 1
8
+        'post_url' => "http://www.example.com",
9
+        'expected_receive_period_in_days' => 1,
10
+        'payload' => {
11
+          'default' => 'value'
12
+        }
10 13
       }
11 14
     }
12 15
 
@@ -17,28 +20,69 @@ describe Agents::PostAgent do
17 20
     @event = Event.new
18 21
     @event.agent = agents(:jane_weather_agent)
19 22
     @event.payload = {
20
-      :somekey => "somevalue",
21
-      :someotherkey => {
22
-        :somekey => "value"
23
+      'somekey' => 'somevalue',
24
+      'someotherkey' => {
25
+        'somekey' => 'value'
23 26
       }
24 27
     }
25 28
 
26
-    @sent_messages = []
27
-    stub.any_instance_of(Agents::PostAgent).post_event { |uri, event| @sent_messages << event }
29
+    @sent_posts = []
30
+    @sent_gets = []
31
+    stub.any_instance_of(Agents::PostAgent).post_data { |data| @sent_posts << data }
32
+    stub.any_instance_of(Agents::PostAgent).get_data { |data| @sent_gets << data }
28 33
   end
29 34
 
30 35
   describe "#receive" do
31
-    it "checks if it can handle multiple events" do
36
+    it "can handle multiple events and merge the payloads with options['payload']" do
32 37
       event1 = Event.new
33 38
       event1.agent = agents(:bob_weather_agent)
34 39
       event1.payload = {
35
-        :xyz => "value1",
36
-        :message => "value2"
40
+        'xyz' => 'value1',
41
+        'message' => 'value2',
42
+        'default' => 'value2'
37 43
       }
38 44
 
39 45
       lambda {
40
-        @checker.receive([@event, event1])
41
-      }.should change { @sent_messages.length }.by(2)
46
+        lambda {
47
+          @checker.receive([@event, event1])
48
+        }.should change { @sent_posts.length }.by(2)
49
+      }.should_not change { @sent_gets.length }
50
+
51
+      @sent_posts[0].should == @event.payload.merge('default' => 'value')
52
+      @sent_posts[1].should == event1.payload
53
+    end
54
+
55
+    it "can make GET requests" do
56
+      @checker.options['method'] = 'get'
57
+
58
+      lambda {
59
+        lambda {
60
+          @checker.receive([@event])
61
+        }.should change { @sent_gets.length }.by(1)
62
+      }.should_not change { @sent_posts.length }
63
+
64
+      @sent_gets[0].should == @event.payload.merge('default' => 'value')
65
+    end
66
+  end
67
+
68
+  describe "#check" do
69
+    it "sends options['payload'] as a POST request" do
70
+      lambda {
71
+        @checker.check
72
+      }.should change { @sent_posts.length }.by(1)
73
+
74
+      @sent_posts[0].should == @checker.options['payload']
75
+    end
76
+
77
+    it "sends options['payload'] as a GET request" do
78
+      @checker.options['method'] = 'get'
79
+      lambda {
80
+        lambda {
81
+          @checker.check
82
+        }.should change { @sent_gets.length }.by(1)
83
+      }.should_not change { @sent_posts.length }
84
+
85
+      @sent_gets[0].should == @checker.options['payload']
42 86
     end
43 87
   end
44 88
 
@@ -59,13 +103,82 @@ describe Agents::PostAgent do
59 103
     end
60 104
 
61 105
     it "should validate presence of post_url" do
62
-      @checker.options[:post_url] = ""
106
+      @checker.options['post_url'] = ""
63 107
       @checker.should_not be_valid
64 108
     end
65 109
 
66 110
     it "should validate presence of expected_receive_period_in_days" do
67
-      @checker.options[:expected_receive_period_in_days] = ""
111
+      @checker.options['expected_receive_period_in_days'] = ""
68 112
       @checker.should_not be_valid
69 113
     end
114
+
115
+    it "should validate method as post or get, defaulting to post" do
116
+      @checker.options['method'] = ""
117
+      @checker.method.should == "post"
118
+      @checker.should be_valid
119
+
120
+      @checker.options['method'] = "POST"
121
+      @checker.method.should == "post"
122
+      @checker.should be_valid
123
+
124
+      @checker.options['method'] = "get"
125
+      @checker.method.should == "get"
126
+      @checker.should be_valid
127
+
128
+      @checker.options['method'] = "wut"
129
+      @checker.method.should == "wut"
130
+      @checker.should_not be_valid
131
+    end
132
+
133
+    it "should validate payload as a hash, if present" do
134
+      @checker.options['payload'] = ""
135
+      @checker.should be_valid
136
+
137
+      @checker.options['payload'] = "hello"
138
+      @checker.should_not be_valid
139
+
140
+      @checker.options['payload'] = ["foo", "bar"]
141
+      @checker.should_not be_valid
142
+
143
+      @checker.options['payload'] = { 'this' => 'that' }
144
+      @checker.should be_valid
145
+    end
146
+
147
+    it "requires headers to be a hash, if present" do
148
+      @checker.options['headers'] = [1,2,3]
149
+      @checker.should_not be_valid
150
+
151
+      @checker.options['headers'] = "hello world"
152
+      @checker.should_not be_valid
153
+
154
+      @checker.options['headers'] = ""
155
+      @checker.should be_valid
156
+
157
+      @checker.options['headers'] = {}
158
+      @checker.should be_valid
159
+
160
+      @checker.options['headers'] = { "Authorization" => "foo bar" }
161
+      @checker.should be_valid
162
+    end
163
+  end
164
+
165
+  describe "#generate_uri" do
166
+    it "merges params with any in the post_url" do
167
+      @checker.options['post_url'] = "http://example.com/a/path?existing_param=existing_value"
168
+      uri = @checker.generate_uri("some_param" => "some_value", "another_param" => "another_value")
169
+      uri.request_uri.should == "/a/path?existing_param=existing_value&some_param=some_value&another_param=another_value"
170
+    end
171
+
172
+    it "works fine with urls that do not have a query" do
173
+      @checker.options['post_url'] = "http://example.com/a/path"
174
+      uri = @checker.generate_uri("some_param" => "some_value", "another_param" => "another_value")
175
+      uri.request_uri.should == "/a/path?some_param=some_value&another_param=another_value"
176
+    end
177
+
178
+    it "just returns the post_uri when no params are given" do
179
+      @checker.options['post_url'] = "http://example.com/a/path?existing_param=existing_value"
180
+      uri = @checker.generate_uri
181
+      uri.request_uri.should == "/a/path?existing_param=existing_value"
182
+    end
70 183
   end
71 184
 end

+ 19 - 14
spec/models/agents/public_transport_agent_spec.rb

@@ -19,7 +19,6 @@ describe Agents::PublicTransportAgent do
19 19
       stub_request(:get, "http://webservices.nextbus.com/service/publicXMLFeed?a=sf-muni&command=predictionsForMultiStops&stops=N%7C5215").
20 20
          with(:headers => {'User-Agent'=>'Typhoeus - https://github.com/typhoeus/typhoeus'}).
21 21
          to_return(:status => 200, :body => File.read(Rails.root.join("spec/data_fixtures/public_transport_agent.xml")), :headers => {})
22
-      stub(Time).now {"2014-01-14 20:21:30 +0500".to_time}
23 22
     end
24 23
 
25 24
     it "should create 4 events" do
@@ -27,15 +26,18 @@ describe Agents::PublicTransportAgent do
27 26
     end
28 27
 
29 28
     it "should add 4 items to memory" do
30
-      @agent.memory.should == {}
31
-      @agent.check
32
-      @agent.memory.should == {"existing_routes" => [
33
-          {"stopTag"=>"5221", "tripTag"=>"5840324", "epochTime"=>"1389706393991", "currentTime"=>"2014-01-14 20:21:30 +0500"},
34
-          {"stopTag"=>"5221", "tripTag"=>"5840083", "epochTime"=>"1389706512784", "currentTime"=>"2014-01-14 20:21:30 +0500"},
35
-          {"stopTag"=>"5215", "tripTag"=>"5840324", "epochTime"=>"1389706282012", "currentTime"=>"2014-01-14 20:21:30 +0500"},
36
-          {"stopTag"=>"5215", "tripTag"=>"5840083", "epochTime"=>"1389706400805", "currentTime"=>"2014-01-14 20:21:30 +0500"}
37
-        ]
38
-      }
29
+      time_travel_to Time.parse("2014-01-14 20:21:30 +0500") do
30
+        @agent.memory.should == {}
31
+        @agent.check
32
+        @agent.save
33
+        @agent.reload.memory.should == {"existing_routes" => [
34
+            {"stopTag"=>"5221", "tripTag"=>"5840324", "epochTime"=>"1389706393991", "currentTime"=>Time.now.to_s},
35
+            {"stopTag"=>"5221", "tripTag"=>"5840083", "epochTime"=>"1389706512784", "currentTime"=>Time.now.to_s},
36
+            {"stopTag"=>"5215", "tripTag"=>"5840324", "epochTime"=>"1389706282012", "currentTime"=>Time.now.to_s},
37
+            {"stopTag"=>"5215", "tripTag"=>"5840083", "epochTime"=>"1389706400805", "currentTime"=>Time.now.to_s}
38
+          ]
39
+        }
40
+      end
39 41
     end
40 42
 
41 43
     it "should not create events twice" do
@@ -44,10 +46,13 @@ describe Agents::PublicTransportAgent do
44 46
     end
45 47
 
46 48
     it "should reset memory after 2 hours" do
47
-      lambda { @agent.check }.should change {@agent.events.count}.by(4)
48
-      stub(Time).now {"2014-01-14 20:21:30 +0500".to_time + 3.hours}
49
-      @agent.cleanup_old_memory
50
-      lambda { @agent.check }.should change {@agent.events.count}.by(4)
49
+      time_travel_to Time.parse("2014-01-14 20:21:30 +0500") do
50
+        lambda { @agent.check }.should change {@agent.events.count}.by(4)
51
+      end
52
+      time_travel_to "2014-01-14 23:21:30 +0500".to_time do
53
+        @agent.cleanup_old_memory
54
+        lambda { @agent.check }.should change {@agent.events.count}.by(4)
55
+      end
51 56
     end
52 57
   end
53 58
 

+ 80 - 0
spec/models/agents/pushbullet_agent_spec.rb

@@ -0,0 +1,80 @@
1
+require 'spec_helper'
2
+require 'models/concerns/json_path_options_overwritable'
3
+
4
+describe Agents::PushbulletAgent do
5
+  it_behaves_like JsonPathOptionsOverwritable
6
+
7
+  before(:each) do
8
+    @valid_params = {
9
+                      'api_key' => 'token',
10
+                      'device_id' => '124',
11
+                      'body_path' => '$.body',
12
+                      'title' => 'hello from huginn'
13
+                    }
14
+
15
+    @checker = Agents::PushbulletAgent.new(:name => "somename", :options => @valid_params)
16
+    @checker.user = users(:jane)
17
+    @checker.save!
18
+
19
+    @event = Event.new
20
+    @event.agent = agents(:bob_weather_agent)
21
+    @event.payload = { :body => 'One two test' }
22
+    @event.save!
23
+  end
24
+
25
+  describe "validating" do
26
+    before do
27
+      @checker.should be_valid
28
+    end
29
+
30
+    it "should require the api_key" do
31
+      @checker.options['api_key'] = nil
32
+      @checker.should_not be_valid
33
+    end
34
+
35
+    it "should require the device_id" do
36
+      @checker.options['device_id'] = nil
37
+      @checker.should_not be_valid
38
+    end
39
+  end
40
+
41
+  describe "helpers" do
42
+    it "it should return the correct basic_options" do
43
+      @checker.send(:basic_options).should == {:basic_auth => {:username =>@checker.options[:api_key], :password=>''},
44
+                                               :body => {:device_iden => @checker.options[:device_id], :type => 'note'}}
45
+    end
46
+
47
+
48
+    it "should return the query_options" do
49
+      @checker.send(:query_options, @event).should == @checker.send(:basic_options).deep_merge({
50
+        :body => {:title => 'hello from huginn', :body => 'One two test'}
51
+      })
52
+    end
53
+  end
54
+
55
+  describe "#receive" do
56
+    it "send a message to the hipchat" do
57
+      stub_request(:post, "https://token:@api.pushbullet.com/api/pushes").
58
+        with(:body => "device_iden=124&type=note&title=hello%20from%20huginn&body=One%20two%20test").
59
+        to_return(:status => 200, :body => "ok", :headers => {})
60
+      dont_allow(@checker).error
61
+      @checker.receive([@event])
62
+    end
63
+
64
+    it "should log resquests which return an error" do
65
+      stub_request(:post, "https://token:@api.pushbullet.com/api/pushes").
66
+        with(:body => "device_iden=124&type=note&title=hello%20from%20huginn&body=One%20two%20test").
67
+        to_return(:status => 200, :body => "error", :headers => {})
68
+      mock(@checker).error("error")
69
+      @checker.receive([@event])
70
+    end
71
+  end
72
+
73
+  describe "#working?" do
74
+    it "should not be working until the first event was received" do
75
+      @checker.should_not be_working
76
+      @checker.last_receive_at = Time.now
77
+      @checker.should be_working
78
+    end
79
+  end
80
+end

+ 222 - 0
spec/models/agents/pushover_agent_spec.rb

@@ -0,0 +1,222 @@
1
+require 'spec_helper'
2
+
3
+describe Agents::PushoverAgent do
4
+  before do
5
+    @checker = Agents::PushoverAgent.new(:name => 'Some Name',
6
+                                       :options => { :token => 'x',
7
+                                                :user => 'x',
8
+                                                :message => 'Some Message',
9
+                                                :device => 'Some Device',
10
+                                                :title => 'Some Message Title',
11
+                                                :url => 'http://someurl.com',
12
+                                                :url_title => 'Some Url Title',
13
+                                                :priority => 0,
14
+                                                :timestamp => 'false',
15
+                                                :sound => 'pushover',
16
+                                                :retry => 0,
17
+                                                :expire => 0,
18
+                                                :expected_receive_period_in_days => '1'})
19
+     
20
+    @checker.user = users(:bob)
21
+    @checker.save!
22
+
23
+    @event = Event.new
24
+    @event.agent = agents(:bob_weather_agent)
25
+    @event.payload = { :message => 'Looks like its going to rain' }
26
+    @event.save!
27
+
28
+    @sent_notifications = []
29
+    stub.any_instance_of(Agents::PushoverAgent).send_notification  { |notification| @sent_notifications << notification}
30
+  end
31
+
32
+  describe '#receive' do
33
+    it 'should make sure multiple events are being received' do
34
+      event1 = Event.new
35
+      event1.agent = agents(:bob_rain_notifier_agent)
36
+      event1.payload = { :message => 'Some message' }
37
+      event1.save!
38
+
39
+      event2 = Event.new
40
+      event2.agent = agents(:bob_weather_agent)
41
+      event2.payload = { :message => 'Some other message' }
42
+      event2.save!
43
+
44
+      @checker.receive([@event,event1,event2])
45
+      @sent_notifications[0]['message'].should == 'Looks like its going to rain'
46
+      @sent_notifications[1]['message'].should == 'Some message'
47
+      @sent_notifications[2]['message'].should == 'Some other message'
48
+    end
49
+
50
+    it 'should make sure event message overrides default message' do
51
+      event = Event.new
52
+      event.agent = agents(:bob_rain_notifier_agent)
53
+      event.payload = { :message => 'Some new message'}
54
+      event.save!
55
+
56
+      @checker.receive([event])
57
+      @sent_notifications[0]['message'].should == 'Some new message'
58
+    end
59
+
60
+    it 'should make sure event text overrides default message' do
61
+      event = Event.new
62
+      event.agent = agents(:bob_rain_notifier_agent)
63
+      event.payload = { :text => 'Some new text'}
64
+      event.save!
65
+
66
+      @checker.receive([event])
67
+      @sent_notifications[0]['message'].should == 'Some new text'
68
+    end
69
+
70
+    it 'should make sure event title overrides default title' do
71
+      event = Event.new
72
+      event.agent = agents(:bob_rain_notifier_agent)
73
+      event.payload = { :message => 'Some message', :title => 'Some new title' }
74
+      event.save!
75
+
76
+      @checker.receive([event])
77
+      @sent_notifications[0]['title'].should == 'Some new title'
78
+    end
79
+
80
+    it 'should make sure event url overrides default url' do
81
+      event = Event.new
82
+      event.agent = agents(:bob_rain_notifier_agent)
83
+      event.payload = { :message => 'Some message', :url => 'Some new url' }
84
+      event.save!
85
+
86
+      @checker.receive([event])
87
+      @sent_notifications[0]['url'].should == 'Some new url'
88
+    end
89
+
90
+    it 'should make sure event url_title overrides default url_title' do
91
+      event = Event.new
92
+      event.agent = agents(:bob_rain_notifier_agent)
93
+      event.payload = { :message => 'Some message', :url_title => 'Some new url_title' }
94
+      event.save!
95
+
96
+      @checker.receive([event])
97
+      @sent_notifications[0]['url_title'].should == 'Some new url_title'
98
+    end
99
+
100
+    it 'should make sure event priority overrides default priority' do
101
+      event = Event.new
102
+      event.agent = agents(:bob_rain_notifier_agent)
103
+      event.payload = { :message => 'Some message', :priority => 1 }
104
+      event.save!
105
+
106
+      @checker.receive([event])
107
+      @sent_notifications[0]['priority'].should == 1
108
+    end
109
+
110
+    it 'should make sure event timestamp overrides default timestamp' do
111
+      event = Event.new
112
+      event.agent = agents(:bob_rain_notifier_agent)
113
+      event.payload = { :message => 'Some message', :timestamp => 'false' }
114
+      event.save!
115
+
116
+      @checker.receive([event])
117
+      @sent_notifications[0]['timestamp'].should == 'false'
118
+    end
119
+
120
+    it 'should make sure event sound overrides default sound' do
121
+      event = Event.new
122
+      event.agent = agents(:bob_rain_notifier_agent)
123
+      event.payload = { :message => 'Some message', :sound => 'Some new sound' }
124
+      event.save!
125
+
126
+      @checker.receive([event])
127
+      @sent_notifications[0]['sound'].should == 'Some new sound'
128
+    end
129
+
130
+    it 'should make sure event retry overrides default retry' do
131
+      event = Event.new
132
+      event.agent = agents(:bob_rain_notifier_agent)
133
+      event.payload = { :message => 'Some message', :retry => 1 }
134
+      event.save!
135
+
136
+      @checker.receive([event])
137
+      @sent_notifications[0]['retry'].should == 1
138
+    end
139
+
140
+    it 'should make sure event expire overrides default expire' do
141
+      event = Event.new
142
+      event.agent = agents(:bob_rain_notifier_agent)
143
+      event.payload = { :message => 'Some message', :expire => 60 }
144
+      event.save!
145
+
146
+      @checker.receive([event])
147
+      @sent_notifications[0]['expire'].should == 60
148
+    end
149
+  end
150
+
151
+  describe '#working?' do
152
+    it 'checks if events have been received within the expected receive period' do
153
+      # No events received
154
+      @checker.should_not be_working 
155
+      Agents::PushoverAgent.async_receive @checker.id, [@event.id]
156
+
157
+      # Just received events
158
+      @checker.reload.should be_working 
159
+      two_days_from_now = 2.days.from_now
160
+      stub(Time).now { two_days_from_now }
161
+      
162
+      # More time has passed than the expected receive period without any new events
163
+      @checker.reload.should_not be_working 
164
+    end
165
+  end
166
+
167
+  describe "validation" do
168
+    before do
169
+      @checker.should be_valid
170
+    end
171
+
172
+    it "should validate presence of token" do
173
+      @checker.options[:token] = ""
174
+      @checker.should_not be_valid
175
+    end
176
+
177
+    it "should validate presence of user" do
178
+      @checker.options[:user] = ""
179
+      @checker.should_not be_valid
180
+    end
181
+
182
+    it "should validate presence of expected_receive_period_in_days" do
183
+      @checker.options[:expected_receive_period_in_days] = ""
184
+      @checker.should_not be_valid
185
+    end
186
+
187
+    it "should make sure device is optional" do
188
+      @checker.options[:device] = ""
189
+      @checker.should be_valid
190
+    end
191
+
192
+    it "should make sure title is optional" do
193
+      @checker.options[:title] = ""
194
+      @checker.should be_valid
195
+    end
196
+
197
+    it "should make sure url is optional" do
198
+      @checker.options[:url] = ""
199
+      @checker.should be_valid
200
+    end
201
+
202
+    it "should make sure url_title is optional" do
203
+      @checker.options[:url_title] = ""
204
+      @checker.should be_valid
205
+    end
206
+
207
+    it "should make sure priority is optional" do
208
+      @checker.options[:priority] = ""
209
+      @checker.should be_valid
210
+    end
211
+
212
+    it "should make sure timestamp is optional" do
213
+      @checker.options[:timestamp] = ""
214
+      @checker.should be_valid
215
+    end
216
+
217
+    it "should make sure sound is optional" do
218
+      @checker.options[:sound] = ""
219
+      @checker.should be_valid
220
+    end
221
+  end
222
+end

+ 99 - 0
spec/models/agents/shell_command_agent_spec.rb

@@ -0,0 +1,99 @@
1
+require 'spec_helper'
2
+
3
+describe Agents::ShellCommandAgent do
4
+  before do
5
+    @valid_path = Dir.pwd
6
+
7
+    @valid_params = {
8
+        :path  => @valid_path,
9
+        :command  => "pwd",
10
+        :expected_update_period_in_days => "1",
11
+      }
12
+
13
+    @checker = Agents::ShellCommandAgent.new(:name => "somename", :options => @valid_params)
14
+    @checker.user = users(:jane)
15
+    @checker.save!
16
+
17
+    @event = Event.new
18
+    @event.agent = agents(:jane_weather_agent)
19
+    @event.payload = {
20
+      :command => "ls"
21
+    }
22
+    @event.save!
23
+
24
+    stub(Agents::ShellCommandAgent).should_run? { true }
25
+  end
26
+
27
+  describe "validation" do
28
+    before do
29
+      @checker.should be_valid
30
+    end
31
+
32
+    it "should validate presence of necessary fields" do
33
+      @checker.options[:command] = nil
34
+      @checker.should_not be_valid
35
+    end
36
+
37
+    it "should validate path" do
38
+      @checker.options[:path] = 'notarealpath/itreallyisnt'
39
+      @checker.should_not be_valid
40
+    end
41
+
42
+    it "should validate path" do
43
+      @checker.options[:path] = '/'
44
+      @checker.should be_valid
45
+    end
46
+  end
47
+
48
+  describe "#working?" do
49
+    it "generating events as scheduled" do
50
+      stub(@checker).run_command(@valid_path, 'pwd') { ["fake pwd output", "", 0] }
51
+
52
+      @checker.should_not be_working
53
+      @checker.check
54
+      @checker.reload.should be_working
55
+      three_days_from_now = 3.days.from_now
56
+      stub(Time).now { three_days_from_now }
57
+      @checker.should_not be_working
58
+    end
59
+  end
60
+
61
+  describe "#check" do
62
+    before do
63
+      stub(@checker).run_command(@valid_path, 'pwd') { ["fake pwd output", "", 0] }
64
+    end
65
+
66
+    it "should create an event when checking" do
67
+      expect { @checker.check }.to change { Event.count }.by(1)
68
+      Event.last.payload[:path].should == @valid_path
69
+      Event.last.payload[:command].should == 'pwd'
70
+      Event.last.payload[:output].should == "fake pwd output"
71
+    end
72
+
73
+    it "does not run when should_run? is false" do
74
+      stub(Agents::ShellCommandAgent).should_run? { false }
75
+      expect { @checker.check }.not_to change { Event.count }
76
+    end
77
+  end
78
+
79
+  describe "#receive" do
80
+    before do
81
+      stub(@checker).run_command(@valid_path, @event.payload[:command]) { ["fake ls output", "", 0] }
82
+    end
83
+
84
+    it "creates events" do
85
+      @checker.receive([@event])
86
+      Event.last.payload[:path].should == @valid_path
87
+      Event.last.payload[:command].should == @event.payload[:command]
88
+      Event.last.payload[:output].should == "fake ls output"
89
+    end
90
+
91
+    it "does not run when should_run? is false" do
92
+      stub(Agents::ShellCommandAgent).should_run? { false }
93
+
94
+      expect {
95
+        @checker.receive([@event])
96
+      }.not_to change { Event.count }
97
+    end
98
+  end
99
+end

+ 67 - 0
spec/models/agents/stubhub_agent_spec.rb

@@ -0,0 +1,67 @@
1
+require 'spec_helper'
2
+
3
+describe Agents::StubhubAgent do
4
+
5
+  let(:name) { 'Agent Name' }
6
+  let(:url) { 'http://www.stubhub.com/event/name-1-1-2014-12345' }
7
+  let(:parsed_body) { JSON.parse(body)['response']['docs'][0] }
8
+  let(:valid_params) { { 'url' => parsed_body['url'] } }
9
+  let(:body) { File.read(Rails.root.join('spec/data_fixtures/stubhub_data.json')) }
10
+  let(:stubhub_event_id) { 12345 }
11
+  let(:response_payload) { {
12
+                            'url' => url,
13
+                            'name' => parsed_body['seo_description_en_US'],
14
+                            'date' => parsed_body['event_date_local'],
15
+                            'max_price' => parsed_body['maxPrice'],
16
+                            'min_price' => parsed_body['minPrice'],
17
+                            'total_postings' => parsed_body['totalPostings'],
18
+                            'total_tickets' => parsed_body['totalTickets'],
19
+                            'venue_name' => parsed_body['venue_name']
20
+                            } }
21
+
22
+  before do
23
+      stub_request(:get, "http://www.stubhub.com/listingCatalog/select/?q=%2B%20stubhubDocumentType:event%0D%0A%2B%20event_id:#{stubhub_event_id}%0D%0A&rows=10&start=0&wt=json").
24
+         to_return(:status => 200, :body => body, :headers => {})
25
+
26
+    @stubhub_agent = described_class.new(name: name, options: valid_params)
27
+    @stubhub_agent.user = users(:jane)
28
+    @stubhub_agent.save!
29
+  end
30
+
31
+
32
+  describe "#check" do
33
+
34
+    it 'should create an event' do
35
+      expect { @stubhub_agent.check }.to change { Event.count }.by(1)
36
+    end
37
+
38
+    it 'should properly parse the response' do
39
+      event = @stubhub_agent.check
40
+      event.payload.should == response_payload
41
+    end
42
+  end
43
+
44
+  describe "validations" do
45
+    before do
46
+      @stubhub_agent.should be_valid
47
+    end
48
+
49
+    it "should require a url" do
50
+      @stubhub_agent.options['url'] = nil
51
+      @stubhub_agent.should_not be_valid
52
+    end
53
+
54
+  end
55
+
56
+  describe "#working?" do
57
+    it "checks if events have been received within the expected receive period" do
58
+      @stubhub_agent.should_not be_working
59
+
60
+      Agents::StubhubAgent.async_check @stubhub_agent.id
61
+      @stubhub_agent.reload.should be_working
62
+      two_days_from_now = 2.days.from_now
63
+      stub(Time).now { two_days_from_now }
64
+      @stubhub_agent.reload.should_not be_working
65
+    end
66
+  end
67
+end

+ 143 - 1
spec/models/agents/trigger_agent_spec.rb

@@ -30,9 +30,32 @@ describe Agents::TriggerAgent do
30 30
       @checker.should be_valid
31 31
     end
32 32
 
33
-    it "should validate presence of options" do
33
+    it "should validate presence of message" do
34 34
       @checker.options['message'] = nil
35 35
       @checker.should_not be_valid
36
+
37
+      @checker.options['message'] = ''
38
+      @checker.should_not be_valid
39
+    end
40
+
41
+    it "should be valid without a message when 'keep_event' is set" do
42
+      @checker.options['keep_event'] = 'true'
43
+      @checker.options['message'] = ''
44
+      @checker.should be_valid
45
+    end
46
+
47
+    it "if present, 'keep_event' must equal true or false" do
48
+      @checker.options['keep_event'] = 'true'
49
+      @checker.should be_valid
50
+
51
+      @checker.options['keep_event'] = 'false'
52
+      @checker.should be_valid
53
+
54
+      @checker.options['keep_event'] = ''
55
+      @checker.should be_valid
56
+
57
+      @checker.options['keep_event'] = 'tralse'
58
+      @checker.should_not be_valid
36 59
     end
37 60
 
38 61
     it "should validate the three fields in each rule" do
@@ -71,6 +94,28 @@ describe Agents::TriggerAgent do
71 94
       }.should change { Event.count }.by(1)
72 95
     end
73 96
 
97
+    it "handles array of regex" do
98
+      @event.payload['foo']['bar']['baz'] = "a222b"
99
+      @checker.options['rules'][0] = {
100
+        'type' => "regex",
101
+        'value' => ["a\\db", "a\\Wb"],
102
+        'path' => "foo.bar.baz",
103
+      }
104
+      lambda {
105
+        @checker.receive([@event])
106
+      }.should_not change { Event.count }
107
+
108
+      @event.payload['foo']['bar']['baz'] = "a2b"
109
+      lambda {
110
+        @checker.receive([@event])
111
+      }.should change { Event.count }.by(1)
112
+
113
+      @event.payload['foo']['bar']['baz'] = "a b"
114
+      lambda {
115
+        @checker.receive([@event])
116
+      }.should change { Event.count }.by(1)
117
+    end
118
+
74 119
     it "handles negated regex" do
75 120
       @event.payload['foo']['bar']['baz'] = "a2b"
76 121
       @checker.options['rules'][0] = {
@@ -89,6 +134,24 @@ describe Agents::TriggerAgent do
89 134
       }.should change { Event.count }.by(1)
90 135
     end
91 136
 
137
+    it "handles array of negated regex" do
138
+      @event.payload['foo']['bar']['baz'] = "a2b"
139
+      @checker.options['rules'][0] = {
140
+        'type' => "!regex",
141
+        'value' => ["a\\db", "a2b"],
142
+        'path' => "foo.bar.baz",
143
+      }
144
+
145
+      lambda {
146
+        @checker.receive([@event])
147
+      }.should_not change { Event.count }
148
+
149
+      @event.payload['foo']['bar']['baz'] = "a3b"
150
+      lambda {
151
+        @checker.receive([@event])
152
+      }.should change { Event.count }.by(1)
153
+    end
154
+
92 155
     it "puts can extract values into the message based on paths" do
93 156
       @checker.receive([@event])
94 157
       Event.last.payload['message'].should == "I saw 'a2b' from Joe"
@@ -109,6 +172,21 @@ describe Agents::TriggerAgent do
109 172
       }.should_not change { Event.count }
110 173
     end
111 174
 
175
+    it "handles array of numerical comparisons" do
176
+      @event.payload['foo']['bar']['baz'] = "5"
177
+      @checker.options['rules'].first['value'] = [6, 3]
178
+      @checker.options['rules'].first['type'] = "field<value"
179
+
180
+      lambda {
181
+        @checker.receive([@event])
182
+      }.should change { Event.count }.by(1)
183
+
184
+      @checker.options['rules'].first['value'] = [4, 3]
185
+      lambda {
186
+        @checker.receive([@event])
187
+      }.should_not change { Event.count }
188
+    end
189
+
112 190
     it "handles exact comparisons" do
113 191
       @event.payload['foo']['bar']['baz'] = "hello world"
114 192
       @checker.options['rules'].first['type'] = "field==value"
@@ -124,6 +202,21 @@ describe Agents::TriggerAgent do
124 202
       }.should change { Event.count }.by(1)
125 203
     end
126 204
 
205
+    it "handles array of exact comparisons" do
206
+      @event.payload['foo']['bar']['baz'] = "hello world"
207
+      @checker.options['rules'].first['type'] = "field==value"
208
+
209
+      @checker.options['rules'].first['value'] = ["hello there", "hello universe"]
210
+      lambda {
211
+        @checker.receive([@event])
212
+      }.should_not change { Event.count }
213
+
214
+      @checker.options['rules'].first['value'] = ["hello world", "hello universe"]
215
+      lambda {
216
+        @checker.receive([@event])
217
+      }.should change { Event.count }.by(1)
218
+    end
219
+
127 220
     it "handles negated comparisons" do
128 221
       @event.payload['foo']['bar']['baz'] = "hello world"
129 222
       @checker.options['rules'].first['type'] = "field!=value"
@@ -140,6 +233,22 @@ describe Agents::TriggerAgent do
140 233
       }.should change { Event.count }.by(1)
141 234
     end
142 235
 
236
+    it "handles array of negated comparisons" do
237
+      @event.payload['foo']['bar']['baz'] = "hello world"
238
+      @checker.options['rules'].first['type'] = "field!=value"
239
+      @checker.options['rules'].first['value'] = ["hello world", "hello world"]
240
+
241
+      lambda {
242
+        @checker.receive([@event])
243
+      }.should_not change { Event.count }
244
+
245
+      @checker.options['rules'].first['value'] = ["hello there", "hello world"]
246
+
247
+      lambda {
248
+        @checker.receive([@event])
249
+      }.should change { Event.count }.by(1)
250
+    end
251
+
143 252
     it "does fine without dots in the path" do
144 253
       @event.payload = { 'hello' => "world" }
145 254
       @checker.options['rules'].first['type'] = "field==value"
@@ -192,5 +301,38 @@ describe Agents::TriggerAgent do
192 301
         @checker.receive([@event])
193 302
       }.should_not change { Event.count }
194 303
     end
304
+
305
+    describe "when 'keep_event' is true" do
306
+      before do
307
+        @checker.options['keep_event'] = 'true'
308
+        @event.payload['foo']['bar']['baz'] = "5"
309
+        @checker.options['rules'].first['type'] = "field<value"
310
+      end
311
+
312
+      it "can re-emit the origin event" do
313
+        @checker.options['rules'].first['value'] = 3
314
+        @checker.options['message'] = ''
315
+        @event.payload['message'] = 'hi there'
316
+
317
+        lambda {
318
+          @checker.receive([@event])
319
+        }.should_not change { Event.count }
320
+
321
+        @checker.options['rules'].first['value'] = 6
322
+        lambda {
323
+          @checker.receive([@event])
324
+        }.should change { Event.count }.by(1)
325
+
326
+        @checker.most_recent_event.payload.should == @event.payload
327
+      end
328
+
329
+      it "merges 'message' into the original event when present" do
330
+        @checker.options['rules'].first['value'] = 6
331
+
332
+        @checker.receive([@event])
333
+
334
+        @checker.most_recent_event.payload.should == @event.payload.merge(:message => "I saw '5' from Joe")
335
+      end
336
+    end
195 337
   end
196 338
 end

+ 184 - 8
spec/models/agents/website_agent_spec.rb

@@ -21,18 +21,71 @@ describe Agents::WebsiteAgent do
21 21
       @checker.save!
22 22
     end
23 23
 
24
-    describe "#check" do
24
+    describe "validations" do
25
+      before do
26
+        @checker.should be_valid
27
+      end
28
+
25 29
       it "should validate the integer fields" do
26
-        @checker.options['expected_update_period_in_days'] = "nonsense"
27
-        lambda { @checker.save! }.should raise_error;
28 30
         @checker.options['expected_update_period_in_days'] = "2"
31
+        @checker.should be_valid
32
+
33
+        @checker.options['expected_update_period_in_days'] = "nonsense"
34
+        @checker.should_not be_valid
35
+      end
36
+
37
+      it "should validate uniqueness_look_back" do
29 38
         @checker.options['uniqueness_look_back'] = "nonsense"
30
-        lambda { @checker.save! }.should raise_error;
39
+        @checker.should_not be_valid
40
+
41
+        @checker.options['uniqueness_look_back'] = "2"
42
+        @checker.should be_valid
43
+      end
44
+
45
+      it "should validate headers" do
46
+        @checker.options['headers'] = "blah"
47
+        @checker.should_not be_valid
48
+
49
+        @checker.options['headers'] = ""
50
+        @checker.should be_valid
51
+
52
+        @checker.options['headers'] = {}
53
+        @checker.should be_valid
54
+
55
+        @checker.options['headers'] = { 'foo' => 'bar' }
56
+        @checker.should be_valid
57
+      end
58
+
59
+      it "should validate mode" do
31 60
         @checker.options['mode'] = "nonsense"
32
-        lambda { @checker.save! }.should raise_error;
33
-        @checker.options = @site
61
+        @checker.should_not be_valid
62
+
63
+        @checker.options['mode'] = "on_change"
64
+        @checker.should be_valid
65
+
66
+        @checker.options['mode'] = "all"
67
+        @checker.should be_valid
68
+
69
+        @checker.options['mode'] = ""
70
+        @checker.should be_valid
71
+      end
72
+
73
+      it "should validate the force_encoding option" do
74
+        @checker.options['force_encoding'] = ''
75
+        @checker.should be_valid
76
+
77
+        @checker.options['force_encoding'] = 'UTF-8'
78
+        @checker.should be_valid
79
+
80
+        @checker.options['force_encoding'] = ['UTF-8']
81
+        @checker.should_not be_valid
82
+
83
+        @checker.options['force_encoding'] = 'UTF-42'
84
+        @checker.should_not be_valid
34 85
       end
35
-    
86
+    end
87
+
88
+    describe "#check" do
36 89
       it "should check for changes (and update Event.expires_at)" do
37 90
         lambda { @checker.check }.should change { Event.count }.by(1)
38 91
         event = Event.last
@@ -81,6 +134,86 @@ describe Agents::WebsiteAgent do
81 134
         @checker.check
82 135
         @checker.logs.first.message.should =~ /Got an uneven number of matches/
83 136
       end
137
+
138
+      it "should accept an array for url" do
139
+        @site['url'] = ["http://xkcd.com/1/", "http://xkcd.com/2/"]
140
+        @checker.options = @site
141
+        lambda { @checker.save! }.should_not raise_error;
142
+        lambda { @checker.check }.should_not raise_error;
143
+      end
144
+
145
+      it "should parse events from all urls in array" do
146
+        lambda {
147
+          @site['url'] = ["http://xkcd.com/", "http://xkcd.com/"]
148
+          @site['mode'] = 'all'
149
+          @checker.options = @site
150
+          @checker.check
151
+        }.should change { Event.count }.by(2)
152
+      end
153
+
154
+      it "should follow unique rules when parsing array of urls" do
155
+        lambda {
156
+          @site['url'] = ["http://xkcd.com/", "http://xkcd.com/"]
157
+          @checker.options = @site
158
+          @checker.check
159
+        }.should change { Event.count }.by(1)
160
+      end
161
+    end
162
+
163
+    describe 'encoding' do
164
+      it 'should be forced with force_encoding option' do
165
+        huginn = "\u{601d}\u{8003}"
166
+        stub_request(:any, /no-encoding/).to_return(:body => {
167
+            :value => huginn,
168
+          }.to_json.encode(Encoding::EUC_JP), :headers => {
169
+            'Content-Type' => 'application/json',
170
+          }, :status => 200)
171
+        site = {
172
+          'name' => "Some JSON Response",
173
+          'expected_update_period_in_days' => 2,
174
+          'type' => "json",
175
+          'url' => "http://no-encoding.example.com",
176
+          'mode' => 'on_change',
177
+          'extract' => {
178
+            'value' => { 'path' => 'value' },
179
+          },
180
+          'force_encoding' => 'EUC-JP',
181
+        }
182
+        checker = Agents::WebsiteAgent.new(:name => "No Encoding Site", :options => site)
183
+        checker.user = users(:bob)
184
+        checker.save!
185
+
186
+        checker.check
187
+        event = Event.last
188
+        event.payload['value'].should == huginn
189
+      end
190
+
191
+      it 'should be overridden with force_encoding option' do
192
+        huginn = "\u{601d}\u{8003}"
193
+        stub_request(:any, /wrong-encoding/).to_return(:body => {
194
+            :value => huginn,
195
+          }.to_json.encode(Encoding::EUC_JP), :headers => {
196
+            'Content-Type' => 'application/json; UTF-8',
197
+          }, :status => 200)
198
+        site = {
199
+          'name' => "Some JSON Response",
200
+          'expected_update_period_in_days' => 2,
201
+          'type' => "json",
202
+          'url' => "http://wrong-encoding.example.com",
203
+          'mode' => 'on_change',
204
+          'extract' => {
205
+            'value' => { 'path' => 'value' },
206
+          },
207
+          'force_encoding' => 'EUC-JP',
208
+        }
209
+        checker = Agents::WebsiteAgent.new(:name => "Wrong Encoding Site", :options => site)
210
+        checker.user = users(:bob)
211
+        checker.save!
212
+
213
+        checker.check
214
+        event = Event.last
215
+        event.payload['value'].should == huginn
216
+      end
84 217
     end
85 218
 
86 219
     describe '#working?' do
@@ -241,11 +374,26 @@ describe Agents::WebsiteAgent do
241 374
         end
242 375
       end
243 376
     end
377
+
378
+    describe "#receive" do
379
+      it "should scrape from the url element in incoming event payload" do
380
+        @event = Event.new
381
+        @event.agent = agents(:bob_rain_notifier_agent)
382
+        @event.payload = { 'url' => "http://xkcd.com" }
383
+
384
+        lambda {
385
+          @checker.options = @site
386
+          @checker.receive([@event])
387
+        }.should change { Event.count }.by(1)
388
+      end
389
+    end
244 390
   end
245 391
 
246 392
   describe "checking with http basic auth" do
247 393
     before do
248
-      stub_request(:any, /user:pass/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/xkcd.html")), :status => 200)
394
+      stub_request(:any, /example/).
395
+        with(headers: { 'Authorization' => "Basic #{['user:pass'].pack('m').chomp}" }).
396
+        to_return(:body => File.read(Rails.root.join("spec/data_fixtures/xkcd.html")), :status => 200)
249 397
       @site = {
250 398
         'name' => "XKCD",
251 399
         'expected_update_period_in_days' => 2,
@@ -271,4 +419,32 @@ describe Agents::WebsiteAgent do
271 419
       end
272 420
     end
273 421
   end
422
+
423
+  describe "checking with headers" do
424
+    before do
425
+      stub_request(:any, /example/).
426
+        with(headers: { 'foo' => 'bar', 'user_agent' => /Faraday/ }).
427
+        to_return(:body => File.read(Rails.root.join("spec/data_fixtures/xkcd.html")), :status => 200)
428
+      @site = {
429
+        'name' => "XKCD",
430
+        'expected_update_period_in_days' => 2,
431
+        'type' => "html",
432
+        'url' => "http://www.example.com",
433
+        'mode' => 'on_change',
434
+        'headers' => { 'foo' => 'bar' },
435
+        'extract' => {
436
+          'url' => { 'css' => "#comic img", 'attr' => "src" },
437
+        }
438
+      }
439
+      @checker = Agents::WebsiteAgent.new(:name => "ua", :options => @site)
440
+      @checker.user = users(:bob)
441
+      @checker.save!
442
+    end
443
+
444
+    describe "#check" do
445
+      it "should check for changes" do
446
+        lambda { @checker.check }.should change { Event.count }.by(1)
447
+      end
448
+    end
449
+  end
274 450
 end

+ 31 - 0
spec/models/concerns/json_path_options_overwritable.rb

@@ -0,0 +1,31 @@
1
+require 'spec_helper'
2
+
3
+shared_examples_for JsonPathOptionsOverwritable do
4
+  before(:each) do
5
+    @valid_params = described_class.new.default_options
6
+
7
+    @checker = described_class.new(:name => "somename", :options => @valid_params)
8
+    @checker.user = users(:jane)
9
+
10
+    @event = Event.new
11
+    @event.agent = agents(:bob_weather_agent)
12
+    @event.payload = { :room_name => 'test room', :message => 'Looks like its going to rain', username: "Huggin user"}
13
+    @event.save!
14
+  end
15
+
16
+  describe "select_option" do
17
+    it "should use the room_name_path if specified" do
18
+      @checker.options['room_name_path'] = "$.room_name"
19
+      @checker.send(:select_option, @event, :room_name).should == "test room"
20
+    end
21
+
22
+    it "should use the normal option when the path option is blank" do
23
+      @checker.options['room_name'] = 'test'
24
+      @checker.send(:select_option, @event, :room_name).should == "test"
25
+    end
26
+  end
27
+
28
+  it "should merge all options" do
29
+    @checker.send(:merge_json_path_options, @event).symbolize_keys.keys.should == @checker.send(:options_with_path)
30
+  end
31
+end

+ 53 - 0
spec/models/concerns/working_helpers.rb

@@ -0,0 +1,53 @@
1
+require 'spec_helper'
2
+
3
+shared_examples_for WorkingHelpers do
4
+  describe "recent_error_logs?" do
5
+    it "returns true if last_error_log_at is near last_event_at" do
6
+      agent = Agent.new
7
+
8
+      agent.last_error_log_at = 10.minutes.ago
9
+      agent.last_event_at = 10.minutes.ago
10
+      agent.recent_error_logs?.should be_true
11
+
12
+      agent.last_error_log_at = 11.minutes.ago
13
+      agent.last_event_at = 10.minutes.ago
14
+      agent.recent_error_logs?.should be_true
15
+
16
+      agent.last_error_log_at = 5.minutes.ago
17
+      agent.last_event_at = 10.minutes.ago
18
+      agent.recent_error_logs?.should be_true
19
+
20
+      agent.last_error_log_at = 15.minutes.ago
21
+      agent.last_event_at = 10.minutes.ago
22
+      agent.recent_error_logs?.should be_false
23
+
24
+      agent.last_error_log_at = 2.days.ago
25
+      agent.last_event_at = 10.minutes.ago
26
+      agent.recent_error_logs?.should be_false
27
+    end
28
+  end
29
+  describe "received_event_without_error?" do
30
+    before do
31
+      @agent = Agent.new
32
+    end
33
+
34
+    it "should return false until the first event was received" do
35
+      @agent.received_event_without_error?.should == false
36
+      @agent.last_receive_at = Time.now
37
+      @agent.received_event_without_error?.should == true
38
+    end
39
+
40
+    it "should return false when the last error occured after the last received event" do
41
+      @agent.last_receive_at = Time.now - 1.minute
42
+      @agent.last_error_log_at = Time.now
43
+      @agent.received_event_without_error?.should == false
44
+    end
45
+
46
+    it "should return true when the last received event occured after the last error" do
47
+      @agent.last_receive_at = Time.now
48
+      @agent.last_error_log_at = Time.now - 1.minute
49
+      @agent.received_event_without_error?.should == true
50
+    end
51
+  end
52
+
53
+end

+ 8 - 2
spec/spec_helper.rb

@@ -1,8 +1,13 @@
1 1
 # This file is copied to spec/ when you run 'rails generate rspec:install'
2 2
 ENV["RAILS_ENV"] ||= 'test'
3 3
 
4
-require 'coveralls'
5
-Coveralls.wear!('rails')
4
+if ENV['COVERAGE']
5
+  require 'simplecov'
6
+  SimpleCov.start 'rails'
7
+else
8
+  require 'coveralls'
9
+  Coveralls.wear!('rails')
10
+end
6 11
 
7 12
 require File.expand_path("../../config/environment", __FILE__)
8 13
 require 'rspec/rails'
@@ -42,4 +47,5 @@ RSpec.configure do |config|
42 47
 
43 48
   config.include Devise::TestHelpers, :type => :controller
44 49
   config.include SpecHelpers
50
+  config.include Delorean
45 51
 end

+ 0 - 0
tmp/.gitkeep