@@ -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 |
############################# |
@@ -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. |
@@ -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) |
@@ -8,32 +8,32 @@ Huginn is a system for building agents that perform automated tasks for you onli |
||
8 | 8 |
|
9 | 9 |
 |
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 |
[](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 |
[](https://travis-ci.org/cantino/huginn) [](https://coveralls.io/r/cantino/huginn) [](https://bitdeli.com/free "Bitdeli Badge") [](https://gemnasium.com/cantino/huginn) [](https://www.bountysource.com/trackers/282580-huginn?utm_source=282580&utm_medium=shield&utm_campaign=TRACKER_BADGE) |
116 | 106 |
|
@@ -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 |
} |
@@ -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) |
@@ -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 |
|
@@ -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 { |
@@ -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 |
@@ -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 |
|
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
@@ -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 |
@@ -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'] = [] |
@@ -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 |
|
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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> |
@@ -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! |
@@ -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" |
@@ -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 |
@@ -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 |
@@ -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 +1 @@ |
||
1 |
-ActiveSupport.on_load :active_record do |
|
2 |
- require 'ar_mysql_column_charset' |
|
3 |
-end |
|
1 |
+require 'ar_mysql_column_charset' |
@@ -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. |
@@ -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." |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -0,0 +1 @@ |
||
1 |
+require 'ar_mysql_column_charset' |
@@ -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 |
@@ -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 |
@@ -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 "unescaping" <characters>\')}); };' |
|
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 "escaping" <characters>', '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 |
@@ -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 |
@@ -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}" |
@@ -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 |