Merge remote-tracking branch 'origin/master' into js_agent_can_be_dryrun

Andrew Cantino 9 gadi atpakaļ
vecāks
revīzija
9a7ebc09b7
41 mainītis faili ar 581 papildinājumiem un 229 dzēšanām
  1. 4 1
      .env.example
  2. 6 3
      Gemfile
  3. 46 22
      Gemfile.lock
  4. 9 19
      README.md
  5. 2 1
      app.json
  6. 8 0
      app/assets/javascripts/pages/agent-edit-page.js.coffee
  7. 15 0
      app/assets/javascripts/pages/agent-show-page.js.coffee
  8. 10 3
      app/assets/stylesheets/application.css.scss.erb
  9. 6 1
      app/concerns/liquid_droppable.rb
  10. 10 0
      app/controllers/agents_controller.rb
  11. 2 0
      app/helpers/application_helper.rb
  12. 15 0
      app/jobs/agent_check_job.rb
  13. 16 0
      app/jobs/agent_receive_job.rb
  14. 6 32
      app/models/agent.rb
  15. 1 1
      app/models/agents/email_agent.rb
  16. 1 1
      app/models/agents/email_digest_agent.rb
  17. 1 1
      app/models/agents/google_calendar_publish_agent.rb
  18. 12 0
      app/models/agents/java_script_agent.rb
  19. 8 6
      app/models/agents/webhook_agent.rb
  20. 11 8
      app/models/agents/website_agent.rb
  21. 78 0
      app/models/agents/wunderlist_agent.rb
  22. 6 2
      app/views/agents/show.html.erb
  23. 1 1
      bin/schedule.rb
  24. 7 1
      bin/setup_heroku
  25. 2 1
      bin/threaded.rb
  26. 3 1
      config/application.rb
  27. 2 2
      config/database.yml
  28. 1 3
      config/initializers/ar_mysql_column_charset.rb
  29. 6 0
      config/initializers/devise.rb
  30. 1 0
      config/locales/devise.en.yml
  31. 1 0
      config/routes.rb
  32. 1 1
      docker/scripts/init
  33. 8 113
      lib/ar_mysql_column_charset.rb
  34. 118 0
      lib/ar_mysql_column_charset/main.rb
  35. 1 0
      lib/tasks/ar_mysql_column_charset.rake
  36. 20 0
      spec/controllers/agents_controller_spec.rb
  37. 1 0
      spec/env.test
  38. 14 0
      spec/models/agents/java_script_agent_spec.rb
  39. 15 5
      spec/models/agents/webhook_agent_spec.rb
  40. 32 0
      spec/models/agents/website_agent_spec.rb
  41. 74 0
      spec/models/agents/wunderlist_agent_spec.rb

+ 4 - 1
.env.example

@@ -19,7 +19,7 @@ DATABASE_ADAPTER=mysql2
19 19
 DATABASE_ENCODING=utf8
20 20
 DATABASE_RECONNECT=true
21 21
 DATABASE_NAME=huginn_development
22
-DATABASE_POOL=5
22
+DATABASE_POOL=10
23 23
 DATABASE_USERNAME=root
24 24
 DATABASE_PASSWORD=""
25 25
 #DATABASE_HOST=your-domain-here.com
@@ -101,6 +101,9 @@ TUMBLR_OAUTH_SECRET=
101 101
 DROPBOX_OAUTH_KEY=
102 102
 DROPBOX_OAUTH_SECRET=
103 103
 
104
+WUNDERLIST_OAUTH_KEY=
105
+WUNDERLIST_OAUTH_SECRET=
106
+
104 107
 #############################
105 108
 #  AWS and Mechanical Turk  #
106 109
 #############################

+ 6 - 3
Gemfile

@@ -8,19 +8,21 @@ gem 'net-ftp-list', '~> 3.2.8'    # FtpsiteAgent
8 8
 gem 'wunderground', '~> 1.2.0'    # WeatherAgent
9 9
 gem 'forecast_io', '~> 2.0.0'     # WeatherAgent
10 10
 gem 'rturk', '~> 2.12.1'          # HumanTaskAgent
11
-gem 'weibo_2', '~> 0.1'         # Weibo Agents
12 11
 gem 'hipchat', '~> 1.2.0'         # HipchatAgent
13 12
 gem 'xmpp4r',  '~> 0.5.6'         # JabberAgent
14 13
 gem 'mqtt'                        # MQTTAgent
15 14
 gem 'slack-notifier', '~> 1.0.0'  # SlackAgent
16 15
 gem 'hypdf', '~> 1.0.7'           # PDFInfoAgent
17 16
 
17
+# Weibo Agents
18
+gem 'weibo_2', github: 'cantino/weibo_2', branch: 'master'
19
+
18 20
 # GoogleCalendarPublishAgent
19 21
 gem "google-api-client", require: 'google/api_client'
20 22
 
21 23
 # Twitter Agents
22
-gem 'twitter', '~> 5.8.0' # Must to be loaded before cantino-twitter-stream.
23
-gem 'twitter-stream', github: 'dsander/twitter-stream', branch: 'huginn'
24
+gem 'twitter', '~> 5.14.0' # Must to be loaded before cantino-twitter-stream.
25
+gem 'twitter-stream', github: 'cantino/twitter-stream', branch: 'huginn'
24 26
 gem 'omniauth-twitter'
25 27
 
26 28
 # Tumblr Agents
@@ -37,6 +39,7 @@ gem 'haversine'
37 39
 # Optional Services.
38 40
 gem 'omniauth-37signals'          # BasecampAgent
39 41
 # gem 'omniauth-github'
42
+gem 'omniauth-wunderlist', github: 'wunderlist/omniauth-wunderlist', ref: 'd0910d0396107b9302aa1bc50e74bb140990ccb8'
40 43
 
41 44
 # Bundler <1.5 does not recognize :x64_mingw as a valid platform name.
42 45
 # Unfortunately, it can't self-update because it errors when encountering :x64_mingw.

+ 46 - 22
Gemfile.lock

@@ -1,12 +1,32 @@
1 1
 GIT
2
-  remote: git://github.com/dsander/twitter-stream.git
3
-  revision: 1713b4fe5b387580364b39716bb5c26d6601c50f
2
+  remote: git://github.com/cantino/twitter-stream.git
3
+  revision: f7e7edb0bae013bffabf3598e7147773d9fd370f
4 4
   branch: huginn
5 5
   specs:
6 6
     twitter-stream (0.1.15)
7 7
       eventmachine (~> 1.0.7)
8 8
       http_parser.rb (~> 0.6.0)
9
-      simple_oauth (~> 0.2.0)
9
+      simple_oauth (~> 0.3.0)
10
+
11
+GIT
12
+  remote: git://github.com/cantino/weibo_2.git
13
+  revision: 00e57d29d8252126014b038cd738b02e05e4cfc5
14
+  branch: master
15
+  specs:
16
+    weibo_2 (0.1.7)
17
+      hashie (~> 2.0.4)
18
+      multi_json (~> 1)
19
+      oauth2 (~> 0.9.1)
20
+      rest-client (~> 1.8)
21
+
22
+GIT
23
+  remote: git://github.com/wunderlist/omniauth-wunderlist.git
24
+  revision: d0910d0396107b9302aa1bc50e74bb140990ccb8
25
+  ref: d0910d0396107b9302aa1bc50e74bb140990ccb8
26
+  specs:
27
+    omniauth-wunderlist (0.0.1)
28
+      omniauth (~> 1.0)
29
+      omniauth-oauth2 (~> 1.1)
10 30
 
11 31
 GEM
12 32
   remote: https://rubygems.org/
@@ -47,7 +67,7 @@ GEM
47 67
       minitest (~> 5.1)
48 68
       thread_safe (~> 0.3, >= 0.3.4)
49 69
       tzinfo (~> 1.1)
50
-    addressable (2.3.7)
70
+    addressable (2.3.8)
51 71
     arel (6.0.0)
52 72
     autoparse (0.3.3)
53 73
       addressable (>= 2.3.1)
@@ -102,6 +122,8 @@ GEM
102 122
       warden (~> 1.2.3)
103 123
     diff-lcs (1.2.5)
104 124
     docile (1.1.5)
125
+    domain_name (0.5.24)
126
+      unf (>= 0.0.5, < 1.0.0)
105 127
     dotenv (0.11.1)
106 128
       dotenv-deployment (~> 0.0.2)
107 129
     dotenv-deployment (0.0.2)
@@ -122,7 +144,7 @@ GEM
122 144
     em-websocket (0.5.1)
123 145
       eventmachine (>= 0.12.9)
124 146
       http_parser.rb (~> 0.6.0)
125
-    equalizer (0.0.9)
147
+    equalizer (0.0.11)
126 148
     erector (0.10.0)
127 149
       treetop (>= 1.2.3)
128 150
     erubis (2.7.0)
@@ -190,8 +212,10 @@ GEM
190 212
     httmultiparty (0.3.10)
191 213
       httparty (>= 0.7.3)
192 214
       multipart-post
193
-    http (0.5.1)
194
-      http_parser.rb
215
+    http (0.6.4)
216
+      http_parser.rb (~> 0.6.0)
217
+    http-cookie (1.0.2)
218
+      domain_name (~> 0.5)
195 219
     http_parser.rb (0.6.0)
196 220
     httparty (0.13.1)
197 221
       json (~> 1.8)
@@ -205,7 +229,7 @@ GEM
205 229
     json (1.8.2)
206 230
     jsonpath (0.5.6)
207 231
       multi_json
208
-    jwt (1.3.0)
232
+    jwt (1.4.1)
209 233
     kaminari (0.16.1)
210 234
       actionpack (>= 3.0.0)
211 235
       activesupport (>= 3.0.0)
@@ -229,7 +253,7 @@ GEM
229 253
     memoizable (0.4.2)
230 254
       thread_safe (~> 0.3, >= 0.3.1)
231 255
     method_source (0.8.2)
232
-    mime-types (2.4.3)
256
+    mime-types (2.5)
233 257
     mini_portile (0.6.2)
234 258
     minitest (5.5.1)
235 259
     mqtt (0.3.1)
@@ -281,7 +305,7 @@ GEM
281 305
       slop (~> 3.4)
282 306
     quiet_assets (1.1.0)
283 307
       railties (>= 3.1, < 5.0)
284
-    rack (1.6.0)
308
+    rack (1.6.1)
285 309
     rack-test (0.6.3)
286 310
       rack (>= 1.0)
287 311
     rails (4.2.1)
@@ -321,7 +345,8 @@ GEM
321 345
     ref (1.0.5)
322 346
     responders (2.1.0)
323 347
       railties (>= 4.2.0, < 5)
324
-    rest-client (1.7.3)
348
+    rest-client (1.8.0)
349
+      http-cookie (>= 1.0.2, < 2.0)
325 350
       mime-types (>= 1.16, < 3.0)
326 351
       netrc (~> 0.7)
327 352
     retriable (2.0.2)
@@ -378,7 +403,7 @@ GEM
378 403
       jwt (>= 0.1.5)
379 404
       multi_json (>= 1.0.0)
380 405
     simple-rss (1.3.1)
381
-    simple_oauth (0.2.0)
406
+    simple_oauth (0.3.1)
382 407
     simplecov (0.9.2)
383 408
       docile (~> 1.1.0)
384 409
       multi_json (~> 1.0)
@@ -426,17 +451,17 @@ GEM
426 451
       builder (>= 2.1.2)
427 452
       jwt (>= 0.1.2)
428 453
       multi_json (>= 1.3.0)
429
-    twitter (5.8.0)
454
+    twitter (5.14.0)
430 455
       addressable (~> 2.3)
431 456
       buftok (~> 0.2.0)
432 457
       equalizer (~> 0.0.9)
433 458
       faraday (~> 0.9.0)
434
-      http (~> 0.5.0)
459
+      http (~> 0.6.0)
435 460
       http_parser.rb (~> 0.6.0)
436 461
       json (~> 1.8)
437 462
       memoizable (~> 0.4.0)
438 463
       naught (~> 1.0)
439
-      simple_oauth (~> 0.2.0)
464
+      simple_oauth (~> 0.3.0)
440 465
     typhoeus (0.6.9)
441 466
       ethon (>= 0.7.1)
442 467
     tzinfo (1.2.2)
@@ -444,6 +469,9 @@ GEM
444 469
     uglifier (2.7.0)
445 470
       execjs (>= 0.3.0)
446 471
       json (>= 1.8.0)
472
+    unf (0.1.4)
473
+      unf_ext
474
+    unf_ext (0.0.7.1)
447 475
     unicorn (4.8.3)
448 476
       kgio (~> 2.6)
449 477
       rack
@@ -457,11 +485,6 @@ GEM
457 485
     webmock (1.17.4)
458 486
       addressable (>= 2.2.7)
459 487
       crack (>= 0.3.2)
460
-    weibo_2 (0.1.7)
461
-      hashie (~> 2.0.4)
462
-      multi_json (~> 1)
463
-      oauth2 (~> 0.9.1)
464
-      rest-client (~> 1.7.3)
465 488
     wunderground (1.2.0)
466 489
       addressable
467 490
       httparty (> 0.6.0)
@@ -521,6 +544,7 @@ DEPENDENCIES
521 544
   omniauth-dropbox
522 545
   omniauth-tumblr
523 546
   omniauth-twitter
547
+  omniauth-wunderlist!
524 548
   pg
525 549
   protected_attributes (~> 1.0.8)
526 550
   pry
@@ -547,7 +571,7 @@ DEPENDENCIES
547 571
   therubyracer (~> 0.12.2)
548 572
   tumblr_client
549 573
   twilio-ruby (~> 3.11.5)
550
-  twitter (~> 5.8.0)
574
+  twitter (~> 5.14.0)
551 575
   twitter-stream!
552 576
   typhoeus (~> 0.6.3)
553 577
   tzinfo (>= 1.2.0)
@@ -556,6 +580,6 @@ DEPENDENCIES
556 580
   unicorn
557 581
   vcr
558 582
   webmock (~> 1.17.4)
559
-  weibo_2 (~> 0.1)
583
+  weibo_2!
560 584
   wunderground (~> 1.2.0)
561 585
   xmpp4r (~> 0.5.6)

+ 9 - 19
README.md

@@ -8,32 +8,32 @@ Huginn is a system for building agents that perform automated tasks for you onli
8 8
 
9 9
 ![the origin of the name](doc/imgs/the-name.png)
10 10
 
11
-#### Here are some of the things that you can do with Huginn right now:
11
+#### Here are some of the things that you can do with Huginn:
12 12
 
13 13
 * Track the weather and get an email when it's going to rain (or snow) tomorrow ("Don't forget your umbrella!")
14
-* List terms that you care about and receive emails when their occurrence on Twitter changes.  (For example, want to know when something interesting has happened in the world of Machine Learning?  Huginn will watch the term "machine learning" on Twitter and tell you when there is a large spike.)
14
+* List terms that you care about and receive emails when their occurrence on Twitter changes.  (For example, want to know when something interesting has happened in the world of Machine Learning?  Huginn will watch the term "machine learning" on Twitter and tell you when there is a spike in discussion.)
15 15
 * Watch for air travel or shopping deals
16 16
 * Follow your project names on Twitter and get updates when people mention them
17 17
 * Scrape websites and receive emails when they change
18 18
 * Connect to Adioso, HipChat, Basecamp, Growl, FTP, IMAP, Jabber, JIRA, MQTT, nextbus, Pushbullet, Pushover, RSS, Bash, Slack, StubHub, translation APIs, Twilio, Twitter, Wunderground, and Weibo, to name a few.
19
-* Compose digest emails about things you care about to be sent at specific times of the day
19
+* Send digest emails with things that you care about at specific times during the day
20 20
 * Track counts of high frequency events and send an SMS within moments when they spike, such as the term "san francisco emergency"
21 21
 * Send and receive WebHooks
22
-* Run arbitrary JavaScript Agents on the server
22
+* Run custom JavaScript or CoffeeScript functions
23 23
 * Track your location over time
24 24
 * Create Amazon Mechanical Turk workflows as the inputs, or outputs, of agents (the Amazon Turk Agent is called the "HumanTaskAgent"). For example: "Once a day, ask 5 people for a funny cat photo; send the results to 5 more people to be rated; send the top-rated photo to 5 people for a funny caption; send to 5 final people to rate for funniest caption; finally, post the best captioned photo on my blog."
25 25
 
26 26
 [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cantino/huginn?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
27 27
 
28
-Follow [@tectonic](https://twitter.com/tectonic) for updates as Huginn evolves, and join us in our [Gitter room](https://gitter.im/cantino/huginn) to discuss the project.
28
+Join us in our [Gitter room](https://gitter.im/cantino/huginn) to discuss the project and follow [@tectonic](https://twitter.com/tectonic) for updates as Huginn evolves.
29 29
 
30
-### We need your help!
30
+### Join us!
31 31
 
32
-Want to help with Huginn?  All contributions are encouraged!  You could make UI improvements, [add new Agents](https://github.com/cantino/huginn/wiki/Creating-a-new-agent), 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).
32
+Want to help with Huginn?  All contributions are encouraged!  You could make UI improvements, [add new Agents](https://github.com/cantino/huginn/wiki/Creating-a-new-agent), write [documentation and tutorials](https://github.com/cantino/huginn/wiki), 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).  Please fork, add specs, and send pull requests!
33 33
 
34
-Really want an issue fixed/feature implemented? Or maybe you just want to solve some community issues and earn some extra coffee money? Then you should take a look at the [current bounties on Bountysource](https://www.bountysource.com/trackers/282580-huginn).
34
+Really want a fix or feature? Want to solve some community issues and earn some extra coffee money? Take a look at the [current bounties on Bountysource](https://www.bountysource.com/trackers/282580-huginn).
35 35
 
36
-Have an awesome an idea but not feeling quite up to contributing yet? Head over to our [Official 'suggest an agent' thread ](https://github.com/cantino/huginn/issues/353) and tell us about your cool idea!
36
+Have an awesome idea but not feeling quite up to contributing yet? Head over to our [Official 'suggest an agent' thread ](https://github.com/cantino/huginn/issues/353) and tell us!
37 37
 
38 38
 ## Examples
39 39
 
@@ -102,15 +102,5 @@ We assume your deployment will run over SSL. This is a very good idea! However,
102 102
 
103 103
 Huginn is provided under the MIT License.
104 104
 
105
-## Community
106
-Huginn has its own IRC channel on freenode: #huginn.
107
-Some of us are hanging out there, come and say hello.
108
-
109
-## Contribution
110
-
111
-Huginn is a work in progress and is just getting started.  Please get involved!  You can [add new Agents](https://github.com/cantino/huginn/wiki/Creating-a-new-agent), expand the [Wiki](https://github.com/cantino/huginn/wiki), or help us simplify and strengthen the Agent API or core application.
112
-
113
-Please fork, add specs, and send pull requests!
114
-
115 105
 [![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) [![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=282580)](https://www.bountysource.com/trackers/282580-huginn?utm_source=282580&utm_medium=shield&utm_campaign=TRACKER_BADGE)
116 106
 

+ 2 - 1
app.json

@@ -4,7 +4,7 @@
4 4
     "website": "https://github.com/cantino/huginn",
5 5
     "repository": "https://github.com/cantino/huginn",
6 6
     "env": {
7
-        "BUILDPACK_URL": "https://github.com/ddollar/heroku-buildpack-multi.git",
7
+        "BUILDPACK_URL": "https://github.com/heroku/heroku-buildpack-multi.git",
8 8
         "APP_SECRET_TOKEN": {
9 9
             "generator": "secret"
10 10
         },
@@ -19,5 +19,6 @@
19 19
     "scripts": {
20 20
       "postdeploy": "bundle exec rake db:migrate"
21 21
     },
22
+    "addons": ["heroku-postgresql"],
22 23
     "success_url": "/users/sign_up"
23 24
 }

+ 8 - 0
app/assets/javascripts/pages/agent-edit-page.js.coffee

@@ -4,6 +4,14 @@ class @AgentEditPage
4 4
     @showCorrectRegionsOnStartup()
5 5
     $("form.agent-form").on "submit", => @updateFromEditors()
6 6
 
7
+    $("#agent_name").each ->
8
+      # Select the number suffix if this is a cloned agent.
9
+      if matches = this.value.match(/ \(\d+\)$/)
10
+        this.focus()
11
+        if this.selectionStart?
12
+          this.selectionStart = matches.index
13
+          this.selectionEnd = this.value.length
14
+
7 15
     # The type selector is only available on the new agent form.
8 16
     if $("#agent_type").length
9 17
       $("#agent_type").on "change", => @handleTypeChange(false)

+ 15 - 0
app/assets/javascripts/pages/agent-show-page.js.coffee

@@ -2,6 +2,7 @@ class @AgentShowPage
2 2
   constructor: ->
3 3
     $(".agent-show #show-tabs a[href='#logs'], #logs .refresh").on "click", @fetchLogs
4 4
     $(".agent-show #logs .clear").on "click", @clearLogs
5
+    $(".agent-show #memory .clear").on "click", @clearMemory
5 6
 
6 7
     # Trigger tabs when navigated to.
7 8
     if tab = window.location.href.match(/tab=(\w+)\b/i)?[1]
@@ -39,6 +40,20 @@ class @AgentShowPage
39 40
         $("#logs .spinner").stop(true, true).fadeOut ->
40 41
           $("#logs .refresh, #logs .clear").show()
41 42
 
43
+  clearMemory: (e) ->
44
+    if confirm("Are you sure you want to clear memory of this Agent?")
45
+      agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
46
+      e.preventDefault()
47
+      $("#memory .spinner").css(display: 'inline-block')
48
+      $("#memory .clear").hide()
49
+      $.post "/agents/#{agentId}/memory", { "_method": "DELETE" }
50
+        .done ->
51
+          $("#memory .spinner").fadeOut ->
52
+            $("#memory + .memory").text "{\n}\n"
53
+        .fail ->
54
+          $("#memory .spinner").fadeOut ->
55
+            $("#memory .clear").css(display: 'inline-block')
56
+
42 57
 $ ->
43 58
   Utils.registerPage(AgentShowPage, forPathsMatching: /^agents\/\d+/)
44 59
 

+ 10 - 3
app/assets/stylesheets/application.css.scss.erb

@@ -61,7 +61,7 @@ img.odin {
61 61
   display: none;
62 62
 }
63 63
 
64
-img.spinner {
64
+.spinner {
65 65
   display: none;
66 66
   vertical-align: bottom;
67 67
 }
@@ -172,6 +172,13 @@ span.not-applicable:after {
172 172
   font-weight: bold;
173 173
 }
174 174
 
175
+// Memory
176
+
177
+#memory .action-icon {
178
+  display: inline-block;
179
+  cursor: pointer;
180
+}
181
+
175 182
 // Credentials and Ace Editor
176 183
 
177 184
 #ace-credential-value {
@@ -251,8 +258,8 @@ h2 .scenario, a span.label.scenario {
251 258
   width: 200px;
252 259
 }
253 260
 
254
-$services:            twitter     37signals   github      tumblr      dropbox;
255
-$service-colors:      #55acee     #8fc857     #444444     #2c4762     #007EE5;
261
+$services:            twitter     37signals   github      tumblr      dropbox   wunderlist;
262
+$service-colors:      #55acee     #8fc857     #444444     #2c4762     #007EE5   #ED5F27;
256 263
 
257 264
 @mixin services {
258 265
   @each $service in $services {

+ 6 - 1
app/concerns/liquid_droppable.rb

@@ -21,7 +21,12 @@ module LiquidDroppable
21 21
   end
22 22
 
23 23
   included do
24
-    const_set :Drop, Kernel.const_set("#{name}Drop", Class.new(Drop)) unless const_defined?("#{name}Drop")
24
+    const_set :Drop,
25
+              if Kernel.const_defined?(drop_name = "#{name}Drop")
26
+                Kernel.const_get(drop_name)
27
+              else
28
+                Kernel.const_set(drop_name, Class.new(Drop))
29
+              end
25 30
   end
26 31
 
27 32
   def to_liquid

+ 10 - 0
app/controllers/agents_controller.rb

@@ -112,6 +112,16 @@ class AgentsController < ApplicationController
112 112
     end
113 113
   end
114 114
 
115
+  def destroy_memory
116
+    @agent = current_user.agents.find(params[:id])
117
+    @agent.update!(memory: {})
118
+
119
+    respond_to do |format|
120
+      format.html { redirect_back "Memory erased for '#{@agent.name}'" }
121
+      format.json { head :ok }
122
+    end
123
+  end
124
+
115 125
   def show
116 126
     @agent = current_user.agents.find(params[:id])
117 127
 

+ 2 - 0
app/helpers/application_helper.rb

@@ -57,6 +57,8 @@ module ApplicationHelper
57 57
     case provider.to_sym
58 58
     when :twitter, :tumblr, :github, :dropbox
59 59
       icon_tag("fa-#{provider}")
60
+    when :wunderlist
61
+      icon_tag("fa-list")
60 62
     else
61 63
       icon_tag("fa-lock")
62 64
     end

+ 15 - 0
app/jobs/agent_check_job.rb

@@ -0,0 +1,15 @@
1
+class AgentCheckJob < ActiveJob::Base
2
+  # Given an Agent id, load the Agent, call #check on it, and then save it with an updated `last_check_at` timestamp.
3
+  def perform(agent_id)
4
+    agent = Agent.find(agent_id)
5
+    begin
6
+      return if agent.unavailable?
7
+      agent.check
8
+      agent.last_check_at = Time.now
9
+      agent.save!
10
+    rescue => e
11
+      agent.error "Exception during check. #{e.message}: #{e.backtrace.join("\n")}"
12
+      raise
13
+    end
14
+  end
15
+end

+ 16 - 0
app/jobs/agent_receive_job.rb

@@ -0,0 +1,16 @@
1
+class AgentReceiveJob < ActiveJob::Base
2
+  # Given an Agent id and an array of Event ids, load the Agent, call #receive on it with the Event objects, and then
3
+  # save it with an updated `last_receive_at` timestamp.
4
+  def perform(agent_id, event_ids)
5
+    agent = Agent.find(agent_id)
6
+    begin
7
+      return if agent.unavailable?
8
+      agent.receive(Event.where(:id => event_ids).order(:id))
9
+      agent.last_receive_at = Time.now
10
+      agent.save!
11
+    rescue => e
12
+      agent.error "Exception during receive. #{e.message}: #{e.backtrace.join("\n")}"
13
+      raise
14
+    end
15
+  end
16
+end

+ 6 - 32
app/models/agent.rb

@@ -387,24 +387,11 @@ class Agent < ActiveRecord::Base
387 387
       end
388 388
     end
389 389
 
390
-    # Given an Agent id and an array of Event ids, load the Agent, call #receive on it with the Event objects, and then
391
-    # save it with an updated `last_receive_at` timestamp.
392
-    #
393
-    # This method is tagged with `handle_asynchronously` and will be delayed and run with delayed_job.  It accepts Agent
394
-    # and Event ids instead of a literal ActiveRecord models because it is preferable to serialize delayed_jobs with ids.
390
+    # This method will enqueue an AgentReceiveJob job. It accepts Agent and Event ids instead of a literal ActiveRecord
391
+    # models because it is preferable to serialize jobs with ids.
395 392
     def async_receive(agent_id, event_ids)
396
-      agent = Agent.find(agent_id)
397
-      begin
398
-        return if agent.unavailable?
399
-        agent.receive(Event.where(:id => event_ids))
400
-        agent.last_receive_at = Time.now
401
-        agent.save!
402
-      rescue => e
403
-        agent.error "Exception during receive. #{e.message}: #{e.backtrace.join("\n")}"
404
-        raise
405
-      end
393
+      AgentReceiveJob.perform_later(agent_id, event_ids)
406 394
     end
407
-    handle_asynchronously :async_receive
408 395
 
409 396
     # Given a schedule name, run `check` via `bulk_check` on all Agents with that schedule.
410 397
     # This is called by bin/schedule.rb for each schedule in `SCHEDULES`.
@@ -425,24 +412,11 @@ class Agent < ActiveRecord::Base
425 412
       end
426 413
     end
427 414
 
428
-    # Given an Agent id, load the Agent, call #check on it, and then save it with an updated `last_check_at` timestamp.
429
-    #
430
-    # This method is tagged with `handle_asynchronously` and will be delayed and run with delayed_job.  It accepts an Agent
431
-    # id instead of a literal Agent because it is preferable to serialize delayed_jobs with ids, instead of with the full
432
-    # Agents.
415
+    # This method will enqueue an AgentCheckJob job. It accepts an Agent id instead of a literal Agent because it is
416
+    # preferable to serialize job with ids, instead of with the full Agents.
433 417
     def async_check(agent_id)
434
-      agent = Agent.find(agent_id)
435
-      begin
436
-        return if agent.unavailable?
437
-        agent.check
438
-        agent.last_check_at = Time.now
439
-        agent.save!
440
-      rescue => e
441
-        agent.error "Exception during check. #{e.message}: #{e.backtrace.join("\n")}"
442
-        raise
443
-      end
418
+      AgentCheckJob.perform_later(agent_id)
444 419
     end
445
-    handle_asynchronously :async_check
446 420
   end
447 421
 end
448 422
 

+ 1 - 1
app/models/agents/email_agent.rb

@@ -35,7 +35,7 @@ module Agents
35 35
       incoming_events.each do |event|
36 36
         log "Sending digest mail to #{user.email} with event #{event.id}"
37 37
         recipients(event.payload).each do |recipient|
38
-          SystemMailer.delay.send_message(:to => recipient, :subject => interpolated(event)['subject'], :headline => interpolated(event)['headline'], :body => interpolated(event)['body'], :groups => [present(event.payload)])
38
+          SystemMailer.send_message(:to => recipient, :subject => interpolated(event)['subject'], :headline => interpolated(event)['headline'], :body => interpolated(event)['body'], :groups => [present(event.payload)]).deliver_later
39 39
         end
40 40
       end
41 41
     end

+ 1 - 1
app/models/agents/email_digest_agent.rb

@@ -42,7 +42,7 @@ module Agents
42 42
         groups = self.memory['queue'].map { |payload| present(payload) }
43 43
         log "Sending digest mail to #{user.email} with events [#{ids}]"
44 44
         recipients.each do |recipient|
45
-          SystemMailer.delay.send_message(:to => recipient, :subject => interpolated['subject'], :headline => interpolated['headline'], :groups => groups)
45
+          SystemMailer.send_message(:to => recipient, :subject => interpolated['subject'], :headline => interpolated['headline'], :groups => groups).deliver_later
46 46
         end
47 47
         self.memory['queue'] = []
48 48
         self.memory['events'] = []

+ 1 - 1
app/models/agents/google_calendar_publish_agent.rb

@@ -18,7 +18,7 @@ module Agents
18 18
       2. New project -> Huginn
19 19
       3. APIs & Auth -> Enable google calendar
20 20
       4. Credentials -> Create new Client ID -> Service Account
21
-      5. Persist the generated private key to a path, ie: `/home/hugin/a822ccdefac89fac6330f95039c492dfa3ce6843.p12`
21
+      5. Persist the generated private key to a path, ie: `/home/huginn/a822ccdefac89fac6330f95039c492dfa3ce6843.p12`
22 22
       6. Grant access via google calendar UI to the service account email address for each calendar you wish to manage. For a whole google apps domain, you can [delegate authority](https://developers.google.com/+/domains/authentication/delegation)
23 23
 
24 24
 

+ 12 - 0
app/models/agents/java_script_agent.rb

@@ -25,6 +25,8 @@ module Agents
25 25
       * `this.options(key)`
26 26
       * `this.log(message)`
27 27
       * `this.error(message)`
28
+      * `this.escapeHtml(htmlToEscape)`
29
+      * `this.unescapeHtml(htmlToUnescape)`
28 30
     MD
29 31
 
30 32
     form_configurable :language, type: :array, values: %w[JavaScript CoffeeScript]
@@ -116,6 +118,8 @@ module Agents
116 118
           memory.to_json
117 119
         end
118 120
       end
121
+      context["escapeHtml"] = lambda { |a, x| CGI.escapeHTML(x) }
122
+      context["unescapeHtml"] = lambda { |a, x| CGI.unescapeHTML(x) }
119 123
 
120 124
       if (options['language'] || '').downcase == 'coffeescript'
121 125
         context.eval(CoffeeScript.compile code)
@@ -176,6 +180,14 @@ module Agents
176 180
           doError(message);
177 181
         }
178 182
 
183
+        Agent.escapeHtml = function(html) {
184
+          return escapeHtml(html);
185
+        }
186
+
187
+        Agent.unescapeHtml = function(html) {
188
+          return unescapeHtml(html);
189
+        }
190
+
179 191
         Agent.check = function(){};
180 192
         Agent.receive = function(){};
181 193
       JS

+ 8 - 6
app/models/agents/webhook_agent.rb

@@ -12,21 +12,20 @@ module Agents
12 12
            https://#{ENV['DOMAIN']}/users/#{user.id}/web_requests/#{id || '<id>'}/:secret
13 13
         ``` where `:secret` is specified in your options.
14 14
 
15
-        The
16
-
17 15
         Options:
18 16
 
19 17
           * `secret` - A token that the host will provide for authentication.
20 18
           * `expected_receive_period_in_days` - How often you expect to receive
21 19
             events this way. Used to determine if the agent is working.
22 20
           * `payload_path` - JSONPath of the attribute in the POST body to be
23
-            used as the Event payload.
21
+            used as the Event payload.  If `payload_path` points to an array,
22
+            Events will be created for each element.
24 23
       MD
25 24
     end
26 25
 
27 26
     event_description do
28 27
       <<-MD
29
-        The event payload is base on the value of the `payload_path` option,
28
+        The event payload is based on the value of the `payload_path` option,
30 29
         which is set to `#{interpolated['payload_path']}`.
31 30
       MD
32 31
     end
@@ -34,7 +33,8 @@ module Agents
34 33
     def default_options
35 34
       { "secret" => "supersecretstring",
36 35
         "expected_receive_period_in_days" => 1,
37
-        "payload_path" => "payload"}
36
+        "payload_path" => "some_key"
37
+      }
38 38
     end
39 39
 
40 40
     def receive_web_request(params, method, format)
@@ -42,7 +42,9 @@ module Agents
42 42
       return ["Please use POST requests only", 401] unless method == "post"
43 43
       return ["Not Authorized", 401] unless secret == interpolated['secret']
44 44
 
45
-      create_event(:payload => payload_for(params))
45
+      [payload_for(params)].flatten.each do |payload|
46
+        create_event(payload: payload)
47
+      end
46 48
 
47 49
       ['Event Created', 201]
48 50
     end

+ 11 - 8
app/models/agents/website_agent.rb

@@ -78,6 +78,8 @@ module Agents
78 78
 
79 79
       Set `disable_ssl_verification` to `true` to disable ssl verification.
80 80
 
81
+      Set `unzip` to `gzip` to inflate the resource using gzip.
82
+
81 83
       The WebsiteAgent can also scrape based on incoming events. It will scrape the url contained in the `url` key of the incoming event payload. If you specify `merge` as the mode, it will retain the old payload and update it with the new values.
82 84
 
83 85
       In Liquid templating, the following variable is available:
@@ -86,7 +88,7 @@ module Agents
86 88
 
87 89
           * `status`: HTTP status as integer. (Almost always 200)
88 90
 
89
-          * `headers`: Reponse headers; for example, `{{ _response_.headers.Content-Type }}` expands to the value of the Content-Type header.  Keys are insentitive to cases and -/_.
91
+          * `headers`: Response headers; for example, `{{ _response_.headers.Content-Type }}` expands to the value of the Content-Type header.  Keys are insensitive to cases and -/_.
90 92
     MD
91 93
 
92 94
     event_description do
@@ -174,6 +176,9 @@ module Agents
174 176
         if (encoding = interpolated['force_encoding']).present?
175 177
           body = body.encode(Encoding::UTF_8, encoding)
176 178
         end
179
+        if interpolated['unzip'] == "gzip"
180
+          body = ActiveSupport::Gzip.decompress(body)
181
+        end
177 182
         doc = parse(body)
178 183
 
179 184
         if extract_full_json?
@@ -240,14 +245,12 @@ module Agents
240 245
       case interpolated['mode'].presence
241 246
       when 'on_change'
242 247
         result_json = result.to_json
243
-        old_events.each do |old_event|
244
-          if old_event.payload.to_json == result_json
245
-            old_event.expires_at = new_event_expiration_date
246
-            old_event.save!
247
-            return false
248
-          end
248
+        if found = old_events.find { |event| event.payload.to_json == result_json }
249
+          found.update!(expires_at: new_event_expiration_date)
250
+          false
251
+        else
252
+          true
249 253
         end
250
-        true
251 254
       when 'all', 'merge', ''
252 255
         true
253 256
       else

+ 78 - 0
app/models/agents/wunderlist_agent.rb

@@ -0,0 +1,78 @@
1
+module Agents
2
+  class WunderlistAgent < Agent
3
+    include FormConfigurable
4
+    include Oauthable
5
+    valid_oauth_providers :wunderlist
6
+
7
+    cannot_be_scheduled!
8
+
9
+    gem_dependency_check { Devise.omniauth_providers.include?(:wunderlist) }
10
+
11
+    description <<-MD
12
+      #{'## Include the `omniauth-wunderlist` gem in your `Gemfile` and set `WUNDERLIST_OAUTH_KEY` and `WUNDERLIST_OAUTH_SECRET` in your environment to use this Agent' if dependencies_missing?}
13
+
14
+      The WunderlistAgent creates new new tasks based on the incoming event.
15
+
16
+      To be able to use this Agent you need to authenticate with Wunderlist in the [Services](/services) section first.
17
+
18
+    MD
19
+
20
+    def default_options
21
+      {
22
+        'list_id' => '',
23
+        'title' => '{{title}}'
24
+      }
25
+    end
26
+
27
+    form_configurable :list_id, roles: :completable
28
+    form_configurable :title
29
+
30
+    def complete_list_id
31
+      response = request_guard do
32
+        HTTParty.get lists_url, request_options
33
+      end
34
+      response.map { |p| {text: "#{p['title']} (#{p['id']})", id: p['id']}}
35
+    end
36
+
37
+    def validate_options
38
+      errors.add(:base, "you need to specify the list you want to add tasks to") unless options['list_id'].present?
39
+      errors.add(:base, "you need to specify the title of the task to create") unless options['title'].present?
40
+    end
41
+
42
+    def working?
43
+      !recent_error_logs?
44
+    end
45
+
46
+    def receive(incoming_events)
47
+      incoming_events.each do |event|
48
+        mo = interpolated(event)
49
+        title = mo[:title][0..244]
50
+        log("Creating new task '#{title}' on list #{mo[:list_id]}", inbound_event: event)
51
+        request_guard do
52
+          HTTParty.post tasks_url, request_options.merge(body: {title: title, list_id: mo[:list_id].to_i}.to_json)
53
+        end
54
+      end
55
+    end
56
+  private
57
+    def request_guard(&blk)
58
+      response = yield
59
+      error("Error during http request: #{response.body}") if response.code > 400
60
+      response
61
+    end
62
+
63
+    def lists_url
64
+      "https://a.wunderlist.com/api/v1/lists"
65
+    end
66
+
67
+    def tasks_url
68
+      "https://a.wunderlist.com/api/v1/tasks"
69
+    end
70
+
71
+    def request_options
72
+      {:headers => {'Content-Type' => 'application/json',
73
+                    'User-Agent' => 'Huginn (https://github.com/cantino/huginn)',
74
+                    'X-Access-Token' => service.token,
75
+                    'X-Client-ID' => ENV["WUNDERLIST_OAUTH_KEY"] }}
76
+    end
77
+  end
78
+end

+ 6 - 2
app/views/agents/show.html.erb

@@ -162,9 +162,13 @@
162 162
               <pre><%= Utils.pretty_jsonify @agent.options || {} %></pre>
163 163
             </p>
164 164
 
165
-            <p>
165
+            <p id="memory" data-agent-id="<%= @agent.id %>">
166 166
               <b>Memory:</b>
167
-              <pre><%= Utils.pretty_jsonify @agent.memory || {} %></pre>
167
+              <% if @agent.memory.present? %>
168
+                <i class="fa fa-spinner fa-pulse spinner"></i>
169
+                <i class="fa fa-trash action-icon clear"></i>
170
+              <% end %>
171
+              <pre class="memory"><%= Utils.pretty_jsonify @agent.memory || {} %></pre>
168 172
             </p>
169 173
           </div>
170 174
         </div>

+ 1 - 1
bin/schedule.rb

@@ -11,5 +11,5 @@ unless defined?(Rails)
11 11
   exit 1
12 12
 end
13 13
 
14
-scheduler = HuginnScheduler.new(frequency: ENV['SCHEDULER_FREQUENCY'])
14
+scheduler = HuginnScheduler.new(frequency: ENV['SCHEDULER_FREQUENCY'].presence || 0.3)
15 15
 scheduler.run!

+ 7 - 1
bin/setup_heroku

@@ -81,10 +81,16 @@ unless $config['DOMAIN']
81 81
   first_time = true
82 82
 end
83 83
 
84
-set_value 'BUILDPACK_URL', "https://github.com/ddollar/heroku-buildpack-multi.git"
84
+set_value 'BUILDPACK_URL', "https://github.com/heroku/heroku-buildpack-multi.git"
85 85
 set_value 'PROCFILE_PATH', "deployment/heroku/Procfile.heroku", force: false
86 86
 set_value 'ON_HEROKU', "true"
87 87
 
88
+unless $config['DATABASE_URL']
89
+  puts "Setting up the postgres addon"
90
+  puts capture("heroku addons:add heroku-postgresql")
91
+  puts
92
+end
93
+
88 94
 unless $config['SMTP_DOMAIN'] && $config['SMTP_USER_NAME'] && $config['SMTP_PASSWORD'] && $config['SMTP_SERVER'] && $config['EMAIL_FROM_ADDRESS']
89 95
   puts "Okay, let's setup outgoing email settings.  The simplest solution is to use the free sendgrid Heroku addon."
90 96
   puts "If you'd like to use your own server, or your Gmail account, please see .env.example and set"

+ 2 - 1
bin/threaded.rb

@@ -1,5 +1,6 @@
1 1
 require 'thread'
2 2
 require 'huginn_scheduler'
3
+require 'twitter_stream'
3 4
 
4 5
 STDOUT.sync = true
5 6
 STDERR.sync = true
@@ -33,7 +34,7 @@ end
33 34
 
34 35
 threads << Thread.new do
35 36
   safely do
36
-    @scheduler = HuginnScheduler.new(frequency: ENV['SCHEDULER_FREQUENCY'])
37
+    @scheduler = HuginnScheduler.new(frequency: ENV['SCHEDULER_FREQUENCY'].presence || 0.3)
37 38
     @scheduler.run!
38 39
     puts "Scheduler stopped ..."
39 40
   end

+ 3 - 1
config/application.rb

@@ -13,7 +13,7 @@ module Huginn
13 13
     # -- all .rb files in that directory are automatically loaded.
14 14
 
15 15
     # Custom directories with classes and modules you want to be autoloadable.
16
-    config.autoload_paths += %W(#{config.root}/lib #{config.root}/app/presenters)
16
+    config.autoload_paths += %W(#{config.root}/lib #{config.root}/app/presenters #{config.root}/app/jobs)
17 17
 
18 18
     # Activate observers that should always be running.
19 19
     # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
@@ -52,5 +52,7 @@ module Huginn
52 52
 
53 53
     # Do not swallow errors in after_commit/after_rollback callbacks.
54 54
     config.active_record.raise_in_transactional_callbacks = true
55
+
56
+    config.active_job.queue_adapter = :delayed_job
55 57
   end
56 58
 end

+ 2 - 2
config/database.yml

@@ -3,7 +3,7 @@ development:
3 3
   encoding: <%= ENV['DATABASE_ENCODING'].presence || "utf8" %>
4 4
   reconnect: <%= ENV['DATABASE_RECONNECT'].presence || "true" %>
5 5
   database: <%= ENV['DATABASE_NAME'].presence || "huginn_development" %>
6
-  pool: <%= ENV['DATABASE_POOL'].presence || "5" %>
6
+  pool: <%= ENV['DATABASE_POOL'].presence || "10" %>
7 7
   username: <%= ENV['DATABASE_USERNAME'].presence || "root" %>
8 8
   password: <%= ENV['DATABASE_PASSWORD'] || "" %>
9 9
   host: <%= ENV['DATABASE_HOST'] || "" %>
@@ -29,7 +29,7 @@ production:
29 29
   encoding: <%= ENV['DATABASE_ENCODING'].presence || "utf8" %>
30 30
   reconnect: <%= ENV['DATABASE_RECONNECT'].presence || "true" %>
31 31
   database: <%= ENV['DATABASE_NAME'].presence || "huginn_production" %>
32
-  pool: <%= ENV['DATABASE_POOL'].presence || "5" %>
32
+  pool: <%= ENV['DATABASE_POOL'].presence || "10" %>
33 33
   username: <%= ENV['DATABASE_USERNAME'].presence || "root" %>
34 34
   password: <%= ENV['DATABASE_PASSWORD'].presence || "password" %>
35 35
   host: <%= ENV['DATABASE_HOST'] || "" %>

+ 1 - 3
config/initializers/ar_mysql_column_charset.rb

@@ -1,3 +1 @@
1
-ActiveSupport.on_load :active_record do
2
-  require 'ar_mysql_column_charset'
3
-end
1
+require 'ar_mysql_column_charset'

+ 6 - 0
config/initializers/devise.rb

@@ -263,6 +263,12 @@ Devise.setup do |config|
263 263
     config.omniauth :dropbox, key, secret
264 264
   end
265 265
 
266
+  if defined?(OmniAuth::Strategies::Wunderlist) &&
267
+     (key = ENV["WUNDERLIST_OAUTH_KEY"]).present? &&
268
+     (secret = ENV["WUNDERLIST_OAUTH_SECRET"]).present?
269
+    config.omniauth :wunderlist, key, secret
270
+  end
271
+
266 272
   # ==> Warden configuration
267 273
   # If you want to use other strategies, that are not supported by Devise, or
268 274
   # change the failure app, you can configure them inside the config.warden block.

+ 1 - 0
config/locales/devise.en.yml

@@ -32,6 +32,7 @@ en:
32 32
       github: "GitHub"
33 33
       37signals: "37Signals (Basecamp)"
34 34
       dropbox: "Dropbox"
35
+      wunderlist: 'Wunderlist'
35 36
     passwords:
36 37
       no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
37 38
       send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."

+ 1 - 0
config/routes.rb

@@ -6,6 +6,7 @@ Huginn::Application.routes.draw do
6 6
       post :handle_details_post
7 7
       put :leave_scenario
8 8
       delete :remove_events
9
+      delete :memory, action: :destroy_memory
9 10
     end
10 11
 
11 12
     collection do

+ 1 - 1
docker/scripts/init

@@ -150,7 +150,7 @@ source /app/.env
150 150
 
151 151
 # Fixup the Procfile and prepare the PORT
152 152
 [ -n "\${DO_NOT_RUN_JOBS}" ] && perl -pi -e 's/^jobs:/#jobs:/' /app/Procfile
153
-perl -pi -e 's/rails server\$/rails server -p \\\$PORT/' /app/Procfile
153
+perl -pi -e 's/rails server\$/rails server -b 0.0.0.0 -p \\\$PORT/' /app/Procfile
154 154
 export PORT
155 155
 
156 156
 # Start huginn

+ 8 - 113
lib/ar_mysql_column_charset.rb

@@ -1,121 +1,16 @@
1
-require 'active_record'
2
-
3 1
 # Module#prepend support for Ruby 1.9
4 2
 require 'prepend' unless Module.method_defined?(:prepend)
5 3
 
6
-module ActiveRecord::ConnectionAdapters
7
-  class ColumnDefinition
8
-    module CharsetSupport
9
-      attr_accessor :charset, :collation
10
-    end
11
-
12
-    prepend CharsetSupport
13
-  end
14
-
15
-  class TableDefinition
16
-    module CharsetSupport
17
-      def new_column_definition(name, type, options)
18
-        column = super
19
-        column.charset   = options[:charset]
20
-        column.collation = options[:collation]
21
-        column
22
-      end
23
-    end
24
-
25
-    prepend CharsetSupport
26
-  end
27
-
28
-  class AbstractMysqlAdapter
29
-    module CharsetSupport
30
-      def prepare_column_options(column, types)
31
-        spec = super
32
-        spec[:charset]   = column.charset.inspect if column.charset && column.charset != charset
33
-        spec[:collation] = column.collation.inspect if column.collation && column.collation != collation
34
-        spec
35
-      end
4
+require 'active_support'
36 5
 
37
-      def migration_keys
38
-        super + [:charset, :collation]
39
-      end
40
-
41
-      def utf8mb4_supported?
42
-        if @utf8mb4_supported.nil?
43
-          @utf8mb4_supported = !select("show character set like 'utf8mb4'").empty?
44
-        else
45
-          @utf8mb4_supported
6
+ActiveSupport.on_load :active_record do
7
+  class << ActiveRecord::Base
8
+    def establish_connection(spec = nil)
9
+      super.tap { |ret|
10
+        if /mysql/i === connection.adapter_name
11
+          require 'ar_mysql_column_charset/main'
46 12
         end
47
-      end
48
-
49
-      def charset_collation(charset, collation)
50
-        [charset, collation].map { |name|
51
-          case name
52
-          when nil
53
-            nil
54
-          when /\A(utf8mb4(_\w*)?)\z/
55
-            if utf8mb4_supported?
56
-              $1
57
-            else
58
-              "utf8#{$2}"
59
-            end
60
-          else
61
-            name.to_s
62
-          end
63
-        }
64
-      end
65
-
66
-      def create_database(name, options = {})
67
-        # utf8mb4 is used in column definitions; use utf8 for
68
-        # databases.
69
-        [:charset, :collation].each { |key|
70
-          case options[key]
71
-          when /\A(utf8mb4(_\w*)?)\z/
72
-            options = options.merge(key => "utf8#{$2}")
73
-          end
74
-        }
75
-        super(name, options)
76
-      end
77
-    end
78
-
79
-    prepend CharsetSupport
80
-
81
-    class SchemaCreation
82
-      module CharsetSupport
83
-        def column_options(o)
84
-          column_options = super
85
-          column_options[:charset]   = o.charset unless o.charset.nil?
86
-          column_options[:collation] = o.collation unless o.collation.nil?
87
-          column_options
88
-        end
89
-
90
-        def add_column_options!(sql, options)
91
-          charset, collation = @conn.charset_collation(options[:charset], options[:collation])
92
-
93
-          if charset
94
-            sql << " CHARACTER SET #{charset}"
95
-          end
96
-
97
-          if collation
98
-            sql << " COLLATE #{collation}"
99
-          end
100
-
101
-          super
102
-        end
103
-      end
104
-
105
-      prepend CharsetSupport
106
-    end
107
-
108
-    class Column
109
-      module CharsetSupport
110
-        attr_reader :charset
111
-
112
-        def initialize(*args)
113
-          super
114
-          @charset = @collation[/\A[^_]+/] unless @collation.nil?
115
-        end
116
-      end
117
-
118
-      prepend CharsetSupport
13
+      }
119 14
     end
120 15
   end
121 16
 end

+ 118 - 0
lib/ar_mysql_column_charset/main.rb

@@ -0,0 +1,118 @@
1
+raise "Do not directly load this library." unless defined?(ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter)
2
+
3
+module ActiveRecord::ConnectionAdapters
4
+  class ColumnDefinition
5
+    module CharsetSupport
6
+      attr_accessor :charset, :collation
7
+    end
8
+
9
+    prepend CharsetSupport
10
+  end
11
+
12
+  class TableDefinition
13
+    module CharsetSupport
14
+      def new_column_definition(name, type, options)
15
+        column = super
16
+        column.charset   = options[:charset]
17
+        column.collation = options[:collation]
18
+        column
19
+      end
20
+    end
21
+
22
+    prepend CharsetSupport
23
+  end
24
+
25
+  class AbstractMysqlAdapter
26
+    module CharsetSupport
27
+      def prepare_column_options(column, types)
28
+        spec = super
29
+        spec[:charset]   = column.charset.inspect if column.charset && column.charset != charset
30
+        spec[:collation] = column.collation.inspect if column.collation && column.collation != collation
31
+        spec
32
+      end
33
+
34
+      def migration_keys
35
+        super + [:charset, :collation]
36
+      end
37
+
38
+      def utf8mb4_supported?
39
+        if @utf8mb4_supported.nil?
40
+          @utf8mb4_supported = !select("show character set like 'utf8mb4'").empty?
41
+        else
42
+          @utf8mb4_supported
43
+        end
44
+      end
45
+
46
+      def charset_collation(charset, collation)
47
+        [charset, collation].map { |name|
48
+          case name
49
+          when nil
50
+            nil
51
+          when /\A(utf8mb4(_\w*)?)\z/
52
+            if utf8mb4_supported?
53
+              $1
54
+            else
55
+              "utf8#{$2}"
56
+            end
57
+          else
58
+            name.to_s
59
+          end
60
+        }
61
+      end
62
+
63
+      def create_database(name, options = {})
64
+        # utf8mb4 is used in column definitions; use utf8 for
65
+        # databases.
66
+        [:charset, :collation].each { |key|
67
+          case options[key]
68
+          when /\A(utf8mb4(_\w*)?)\z/
69
+            options = options.merge(key => "utf8#{$2}")
70
+          end
71
+        }
72
+        super(name, options)
73
+      end
74
+    end
75
+
76
+    prepend CharsetSupport
77
+
78
+    class SchemaCreation
79
+      module CharsetSupport
80
+        def column_options(o)
81
+          column_options = super
82
+          column_options[:charset]   = o.charset unless o.charset.nil?
83
+          column_options[:collation] = o.collation unless o.collation.nil?
84
+          column_options
85
+        end
86
+
87
+        def add_column_options!(sql, options)
88
+          charset, collation = @conn.charset_collation(options[:charset], options[:collation])
89
+
90
+          if charset
91
+            sql << " CHARACTER SET #{charset}"
92
+          end
93
+
94
+          if collation
95
+            sql << " COLLATE #{collation}"
96
+          end
97
+
98
+          super
99
+        end
100
+      end
101
+
102
+      prepend CharsetSupport
103
+    end
104
+
105
+    class Column
106
+      module CharsetSupport
107
+        attr_reader :charset
108
+
109
+        def initialize(*args)
110
+          super
111
+          @charset = @collation[/\A[^_]+/] unless @collation.nil?
112
+        end
113
+      end
114
+
115
+      prepend CharsetSupport
116
+    end
117
+  end
118
+end

+ 1 - 0
lib/tasks/ar_mysql_column_charset.rake

@@ -0,0 +1 @@
1
+require 'ar_mysql_column_charset'

+ 20 - 0
spec/controllers/agents_controller_spec.rb

@@ -374,4 +374,24 @@ describe AgentsController do
374 374
       }
375 375
     end
376 376
   end
377
+
378
+  describe "DELETE memory" do
379
+    it "clears memory of the agent" do
380
+      agent = agents(:bob_website_agent)
381
+      agent.update!(memory: { "test" => 42 })
382
+      sign_in users(:bob)
383
+      delete :destroy_memory, id: agent.to_param
384
+      expect(agent.reload.memory).to eq({})
385
+    end
386
+
387
+    it "does not clear memory of an agent not owned by the current user" do
388
+      agent = agents(:jane_website_agent)
389
+      agent.update!(memory: { "test" => 42 })
390
+      sign_in users(:bob)
391
+      expect {
392
+        delete :destroy_memory, id: agent.to_param
393
+      }.to raise_error(ActiveRecord::RecordNotFound)
394
+      expect(agent.reload.memory).to eq({ "test" => 42})
395
+    end
396
+  end
377 397
 end

+ 1 - 0
spec/env.test

@@ -7,4 +7,5 @@ THIRTY_SEVEN_SIGNALS_OAUTH_KEY=TESTKEY
7 7
 THIRTY_SEVEN_SIGNALS_OAUTH_SECRET=TESTSECRET
8 8
 DROPBOX_OAUTH_KEY=dropboxoauthkey
9 9
 DROPBOX_OAUTH_SECRET=dropboxoauthsecret
10
+WUNDERLIST_OAUTH_KEY=wunderoauthkey
10 11
 FAILED_JOBS_TO_KEEP=2

+ 14 - 0
spec/models/agents/java_script_agent_spec.rb

@@ -176,6 +176,20 @@ describe Agents::JavaScriptAgent do
176 176
       end
177 177
     end
178 178
 
179
+    describe "escaping and unescaping HTML" do
180
+      it "can escape and unescape html with this.escapeHtml and this.unescapeHtml in the javascript environment" do
181
+        @agent.options['code'] = 'Agent.check = function() { this.createEvent({ escaped: this.escapeHtml(\'test \"escaping\" <characters>\'), unescaped: this.unescapeHtml(\'test &quot;unescaping&quot; &lt;characters&gt;\')}); };'
182
+        @agent.save!
183
+        expect {
184
+          expect {
185
+            @agent.check
186
+          }.not_to change { AgentLog.count }
187
+        }.to change { Event.count}.by(1)
188
+        created_event = @agent.events.last
189
+        expect(created_event.payload).to eq({ 'escaped' => 'test &quot;escaping&quot; &lt;characters&gt;', 'unescaped' => 'test "unescaping" <characters>'})
190
+      end
191
+    end
192
+
179 193
     describe "getting incoming events" do
180 194
       it "can access incoming events in the JavaScript enviroment via this.incomingEvents" do
181 195
         event = Event.new

+ 15 - 5
spec/models/agents/webhook_agent_spec.rb

@@ -3,27 +3,37 @@ require 'spec_helper'
3 3
 describe Agents::WebhookAgent do
4 4
   let(:agent) do
5 5
     _agent = Agents::WebhookAgent.new(:name => 'webhook',
6
-                                      :options => { 'secret' => 'foobar', 'payload_path' => 'payload' })
6
+                                      :options => { 'secret' => 'foobar', 'payload_path' => 'some_key' })
7 7
     _agent.user = users(:bob)
8 8
     _agent.save!
9 9
     _agent
10 10
   end
11
-  let(:payload) { {'some' => 'info'} }
11
+  let(:payload) { {'people' => [{ 'name' => 'bob' }, { 'name' => 'jon' }] } }
12 12
 
13 13
   describe 'receive_web_request' do
14 14
     it 'should create event if secret matches' do
15 15
       out = nil
16 16
       expect {
17
-        out = agent.receive_web_request({ 'secret' => 'foobar', 'payload' => payload }, "post", "text/html")
17
+        out = agent.receive_web_request({ 'secret' => 'foobar', 'some_key' => payload }, "post", "text/html")
18 18
       }.to change { Event.count }.by(1)
19 19
       expect(out).to eq(['Event Created', 201])
20 20
       expect(Event.last.payload).to eq(payload)
21 21
     end
22 22
 
23
+    it 'should be able to create multiple events when given an array' do
24
+      out = nil
25
+      agent.options['payload_path'] = 'some_key.people'
26
+      expect {
27
+        out = agent.receive_web_request({ 'secret' => 'foobar', 'some_key' => payload }, "post", "text/html")
28
+      }.to change { Event.count }.by(2)
29
+      expect(out).to eq(['Event Created', 201])
30
+      expect(Event.last.payload).to eq({ 'name' => 'jon' })
31
+    end
32
+
23 33
     it 'should not create event if secrets dont match' do
24 34
       out = nil
25 35
       expect {
26
-        out = agent.receive_web_request({ 'secret' => 'bazbat', 'payload' => payload }, "post", "text/html")
36
+        out = agent.receive_web_request({ 'secret' => 'bazbat', 'some_key' => payload }, "post", "text/html")
27 37
       }.to change { Event.count }.by(0)
28 38
       expect(out).to eq(['Not Authorized', 401])
29 39
     end
@@ -31,7 +41,7 @@ describe Agents::WebhookAgent do
31 41
     it "should only accept POSTs" do
32 42
       out = nil
33 43
       expect {
34
-        out = agent.receive_web_request({ 'secret' => 'foobar', 'payload' => payload }, "get", "text/html")
44
+        out = agent.receive_web_request({ 'secret' => 'foobar', 'some_key' => payload }, "get", "text/html")
35 45
       }.to change { Event.count }.by(0)
36 46
       expect(out).to eq(['Please use POST requests only', 401])
37 47
     end

+ 32 - 0
spec/models/agents/website_agent_spec.rb

@@ -152,6 +152,38 @@ describe Agents::WebsiteAgent do
152 152
       end
153 153
     end
154 154
 
155
+    describe 'unzipping' do
156
+      it 'should unzip with unzip option' do
157
+        json = {
158
+          'response' => {
159
+            'version' => 2,
160
+            'title' => "hello!"
161
+          }
162
+        }
163
+        zipped = ActiveSupport::Gzip.compress(json.to_json)
164
+        stub_request(:any, /gzip/).to_return(:body => zipped, :status => 200)
165
+        site = {
166
+          'name' => "Some JSON Response",
167
+          'expected_update_period_in_days' => "2",
168
+          'type' => "json",
169
+          'url' => "http://gzip.com",
170
+          'mode' => 'on_change',
171
+          'extract' => {
172
+            'version' => { 'path' => 'response.version' },
173
+          },
174
+          'unzip' => 'gzip',
175
+        }
176
+        checker = Agents::WebsiteAgent.new(:name => "Weather Site", :options => site)
177
+        checker.user = users(:bob)
178
+        checker.save!
179
+
180
+        checker.check
181
+        event = Event.last
182
+        puts event.payload
183
+        expect(event.payload['version']).to eq(2)
184
+      end
185
+    end
186
+
155 187
     describe 'encoding' do
156 188
       it 'should be forced with force_encoding option' do
157 189
         huginn = "\u{601d}\u{8003}"

+ 74 - 0
spec/models/agents/wunderlist_agent_spec.rb

@@ -0,0 +1,74 @@
1
+require 'spec_helper'
2
+require 'models/concerns/oauthable'
3
+
4
+describe Agents::WunderlistAgent do
5
+  it_behaves_like Oauthable
6
+
7
+  before(:each) do
8
+
9
+    @valid_params = {
10
+                      'list_id' => '12345',
11
+                      'title' => '{{title}}: {{url}}',
12
+                    }
13
+
14
+    @checker = Agents::WunderlistAgent.new(:name => "somename", :options => @valid_params)
15
+    @checker.user = users(:jane)
16
+    @checker.service = services(:generic)
17
+    @checker.save!
18
+
19
+    @event = Event.new
20
+    @event.agent = agents(:bob_weather_agent)
21
+    @event.payload = { title: 'hello', url: 'www.example.com'}
22
+    @event.save!
23
+  end
24
+
25
+  describe "validating" do
26
+    before do
27
+      expect(@checker).to be_valid
28
+    end
29
+
30
+    it "should require the title" do
31
+      @checker.options['title'] = nil
32
+      expect(@checker).not_to be_valid
33
+    end
34
+
35
+    it "should require the list_id" do
36
+      @checker.options['list_id'] = nil
37
+      expect(@checker).not_to be_valid
38
+    end
39
+  end
40
+
41
+  it "should generate the request_options" do
42
+    expect(@checker.send(:request_options)).to eq({:headers=>{"Content-Type"=>"application/json", "User-Agent"=>"Huginn (https://github.com/cantino/huginn)", "X-Access-Token"=>"1234token", "X-Client-ID"=>"wunderoauthkey"}})
43
+  end
44
+
45
+  describe "#complete_list_id" do
46
+    it "should return a array of hashes" do
47
+      stub_request(:get, 'https://a.wunderlist.com/api/v1/lists').to_return(
48
+        :body => JSON.dump([{title: 'test', id: 12345}]),
49
+        :headers => {"Content-Type" => "text/json"}
50
+      )
51
+      expect(@checker.complete_list_id).to eq([{:text=>"test (12345)", :id=>12345}])
52
+    end
53
+  end
54
+
55
+  describe "#receive" do
56
+    it "send a message to the hipchat" do
57
+      stub_request(:post, 'https://a.wunderlist.com/api/v1/tasks').with { |request| request.body == 'abc'}
58
+      @checker.receive([@event])
59
+    end
60
+  end
61
+
62
+  describe "#working?" do
63
+    it "should be working with no entry in the error log" do
64
+      expect(@checker).to be_working
65
+    end
66
+
67
+    it "should not be working with a recent entry in the error log" do
68
+      @checker.error("test")
69
+      @checker.reload
70
+      @checker.last_event_at = Time.now
71
+      expect(@checker).to_not be_working
72
+    end
73
+  end
74
+end