@@ -31,6 +31,7 @@ gem 'json', '~> 1.8.1' |
||
31 | 31 |
gem 'jsonpath', '~> 0.5.3' |
32 | 32 |
gem 'twilio-ruby', '~> 3.11.5' |
33 | 33 |
gem 'ruby-growl', '~> 4.1.0' |
34 |
+gem 'liquid', '~> 2.6.1' |
|
34 | 35 |
|
35 | 36 |
gem 'delayed_job', '~> 4.0.0' |
36 | 37 |
gem 'delayed_job_active_record', '~> 4.0.0' |
@@ -148,6 +148,7 @@ GEM |
||
148 | 148 |
activesupport (>= 3.0.0) |
149 | 149 |
kramdown (1.3.3) |
150 | 150 |
libv8 (3.16.14.3) |
151 |
+ liquid (2.6.1) |
|
151 | 152 |
macaddr (1.7.1) |
152 | 153 |
systemu (~> 2.6.2) |
153 | 154 |
mail (2.5.4) |
@@ -338,6 +339,7 @@ DEPENDENCIES |
||
338 | 339 |
jsonpath (~> 0.5.3) |
339 | 340 |
kaminari (~> 0.15.1) |
340 | 341 |
kramdown (~> 1.3.3) |
342 |
+ liquid (~> 2.6.1) |
|
341 | 343 |
mysql2 (~> 0.3.15) |
342 | 344 |
nokogiri (~> 1.6.1) |
343 | 345 |
protected_attributes (~> 1.0.7) |
@@ -1,39 +0,0 @@ |
||
1 |
-module JsonPathOptionsOverwritable |
|
2 |
- extend ActiveSupport::Concern |
|
3 |
- # Using this concern allows providing optional `<attribute>_path` options hash |
|
4 |
- # attributes which will then (if not blank) be interpolated using the provided JSONPath. |
|
5 |
- # |
|
6 |
- # Example options Hash: |
|
7 |
- # { |
|
8 |
- # name: 'Huginn', |
|
9 |
- # name_path: '$.name', |
|
10 |
- # title: 'Hello from Huginn' |
|
11 |
- # title_path: '' |
|
12 |
- # } |
|
13 |
- # Example event payload: |
|
14 |
- # { |
|
15 |
- # name: 'dynamic huginn' |
|
16 |
- # } |
|
17 |
- # calling agent.merge_json_path_options(event) returns the following hash: |
|
18 |
- # { |
|
19 |
- # name: 'dynamic huginn' |
|
20 |
- # title: 'Hello from Huginn' |
|
21 |
- # } |
|
22 |
- |
|
23 |
- private |
|
24 |
- def merge_json_path_options(event) |
|
25 |
- options.select { |k, v| options_with_path.include? k}.tap do |merged_options| |
|
26 |
- options_with_path.each do |a| |
|
27 |
- merged_options[a] = select_option(event, a) |
|
28 |
- end |
|
29 |
- end |
|
30 |
- end |
|
31 |
- |
|
32 |
- def select_option(event, a) |
|
33 |
- if options[a.to_s + '_path'].present? |
|
34 |
- Utils.value_at(event.payload, options[a.to_s + '_path']) |
|
35 |
- else |
|
36 |
- options[a] |
|
37 |
- end |
|
38 |
- end |
|
39 |
-end |
@@ -0,0 +1,34 @@ |
||
1 |
+module LiquidInterpolatable |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ def interpolate_options(options, payload) |
|
5 |
+ case options.class.to_s |
|
6 |
+ when 'String' |
|
7 |
+ Liquid::Template.parse(options).render(payload) |
|
8 |
+ when 'ActiveSupport::HashWithIndifferentAccess', 'Hash' |
|
9 |
+ duped_options = options.dup |
|
10 |
+ duped_options.each do |key, value| |
|
11 |
+ duped_options[key] = interpolate_options(value, payload) |
|
12 |
+ end |
|
13 |
+ when 'Array' |
|
14 |
+ options.collect do |value| |
|
15 |
+ interpolate_options(value, payload) |
|
16 |
+ end |
|
17 |
+ end |
|
18 |
+ end |
|
19 |
+ |
|
20 |
+ def interpolate_string(string, payload) |
|
21 |
+ Liquid::Template.parse(string).render(payload) |
|
22 |
+ end |
|
23 |
+ |
|
24 |
+ require 'uri' |
|
25 |
+ # Percent encoding for URI conforming to RFC 3986. |
|
26 |
+ # Ref: http://tools.ietf.org/html/rfc3986#page-12 |
|
27 |
+ module Huginn |
|
28 |
+ def uri_escape(string) |
|
29 |
+ CGI::escape string |
|
30 |
+ end |
|
31 |
+ end |
|
32 |
+ |
|
33 |
+ Liquid::Template.register_filter(LiquidInterpolatable::Huginn) |
|
34 |
+end |
@@ -121,10 +121,6 @@ class Agent < ActiveRecord::Base |
||
121 | 121 |
end |
122 | 122 |
end |
123 | 123 |
|
124 |
- def make_message(payload, message = options[:message]) |
|
125 |
- message.gsub(/<([^>]+)>/) { Utils.value_at(payload, $1) || "??" } |
|
126 |
- end |
|
127 |
- |
|
128 | 124 |
def trigger_web_request(params, method, format) |
129 | 125 |
if respond_to?(:receive_webhook) |
130 | 126 |
Rails.logger.warn "DEPRECATED: The .receive_webhook method is deprecated, please switch your Agent to use .receive_web_request." |
@@ -1,5 +1,7 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class DataOutputAgent < Agent |
3 |
+ include LiquidInterpolatable |
|
4 |
+ |
|
3 | 5 |
cannot_be_scheduled! |
4 | 6 |
|
5 | 7 |
description do |
@@ -19,7 +21,7 @@ module Agents |
||
19 | 21 |
|
20 | 22 |
* `secrets` - An array of tokens that the requestor must provide for light-weight authentication. |
21 | 23 |
* `expected_receive_period_in_days` - How often you expect data to be received by this Agent from other Agents. |
22 |
- * `template` - A JSON object representing a mapping between item output keys and incoming event JSONPath values. JSONPath values must start with `$`, or can be interpolated between `<` and `>` characters. The `item` key will be repeated for every Event. |
|
24 |
+ * `template` - A JSON object representing a mapping between item output keys and incoming event values. Use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to format the values. The `item` key will be repeated for every Event. |
|
23 | 25 |
MD |
24 | 26 |
end |
25 | 27 |
|
@@ -31,9 +33,9 @@ module Agents |
||
31 | 33 |
"title" => "XKCD comics as a feed", |
32 | 34 |
"description" => "This is a feed of recent XKCD comics, generated by Huginn", |
33 | 35 |
"item" => { |
34 |
- "title" => "$.title", |
|
35 |
- "description" => "Secret hovertext: <$.hovertext>", |
|
36 |
- "link" => "$.url", |
|
36 |
+ "title" => "{{title}}", |
|
37 |
+ "description" => "Secret hovertext: {{hovertext}}", |
|
38 |
+ "link" => "{{url}}", |
|
37 | 39 |
} |
38 | 40 |
} |
39 | 41 |
} |
@@ -82,7 +84,7 @@ module Agents |
||
82 | 84 |
def receive_web_request(params, method, format) |
83 | 85 |
if options['secrets'].include?(params['secret']) |
84 | 86 |
items = received_events.order('id desc').limit(events_to_show).map do |event| |
85 |
- interpolated = Utils.recursively_interpolate_jsonpaths(options['template']['item'], event.payload, :leading_dollarsign_is_jsonpath => true) |
|
87 |
+ interpolated = interpolate_options(options['template']['item'], event.payload) |
|
86 | 88 |
interpolated['guid'] = event.id |
87 | 89 |
interpolated['pubDate'] = event.created_at.rfc2822.to_s |
88 | 90 |
interpolated |
@@ -1,5 +1,6 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class EventFormattingAgent < Agent |
3 |
+ include LiquidInterpolatable |
|
3 | 4 |
cannot_be_scheduled! |
4 | 5 |
|
5 | 6 |
description <<-MD |
@@ -24,11 +25,11 @@ module Agents |
||
24 | 25 |
You can use an Event Formatting Agent's `instructions` setting to do this in the following way: |
25 | 26 |
|
26 | 27 |
"instructions": { |
27 |
- "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius.", |
|
28 |
- "subject": "$.data" |
|
28 |
+ "message": "Today's conditions look like {{conditions}} with a high temperature of {{high.celsius}} degrees Celsius.", |
|
29 |
+ "subject": "{{data}}" |
|
29 | 30 |
} |
30 | 31 |
|
31 |
- JSONPaths must be between < and > . Make sure that you don't use these symbols anywhere else. |
|
32 |
+ Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating. |
|
32 | 33 |
|
33 | 34 |
Events generated by this possible Event Formatting Agent will look like: |
34 | 35 |
|
@@ -42,7 +43,7 @@ module Agents |
||
42 | 43 |
{ |
43 | 44 |
"matchers": [ |
44 | 45 |
{ |
45 |
- "path": "$.date.pretty", |
|
46 |
+ "path": "{{date.pretty}}", |
|
46 | 47 |
"regexp": "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)", |
47 | 48 |
"to": "pretty_date", |
48 | 49 |
} |
@@ -60,18 +61,18 @@ module Agents |
||
60 | 61 |
So you can use it in `instructions` like this: |
61 | 62 |
|
62 | 63 |
"instructions": { |
63 |
- "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius according to the forecast at <$.pretty_date.time>.", |
|
64 |
- "subject": "$.data" |
|
64 |
+ "message": "Today's conditions look like <$.conditions> with a high temperature of {{high.celsius}} degrees Celsius according to the forecast at {{pretty_date.time}}.", |
|
65 |
+ "subject": "{{data}}" |
|
65 | 66 |
} |
66 | 67 |
|
67 | 68 |
If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`. |
68 | 69 |
|
69 | 70 |
By default, the output event will have `agent` and `created_at` fields added as well, reflecting the original Agent type and Event creation time. You can skip these outputs by setting `skip_agent` and `skip_created_at` to `true`. |
70 | 71 |
|
71 |
- To CGI escape output (for example when creating a link), prefix with `escape`, like so: |
|
72 |
+ To CGI escape output (for example when creating a link), use the Liquid `uri_escape` filter, like so: |
|
72 | 73 |
|
73 | 74 |
{ |
74 |
- "message": "A peak was on Twitter in <$.group_by>. Search: https://twitter.com/search?q=<escape $.group_by>" |
|
75 |
+ "message": "A peak was on Twitter in {{group_by}}. Search: https://twitter.com/search?q={{group_by | uri_escape}}" |
|
75 | 76 |
} |
76 | 77 |
MD |
77 | 78 |
|
@@ -88,8 +89,8 @@ module Agents |
||
88 | 89 |
def default_options |
89 | 90 |
{ |
90 | 91 |
'instructions' => { |
91 |
- 'message' => "You received a text <$.text> from <$.fields.from>", |
|
92 |
- 'some_other_field' => "Looks like the weather is going to be <$.fields.weather>" |
|
92 |
+ 'message' => "You received a text {{text}} from {{fields.from}}", |
|
93 |
+ 'some_other_field' => "Looks like the weather is going to be {{fields.weather}}" |
|
93 | 94 |
}, |
94 | 95 |
'matchers' => [], |
95 | 96 |
'mode' => "clean", |
@@ -106,7 +107,7 @@ module Agents |
||
106 | 107 |
incoming_events.each do |event| |
107 | 108 |
formatted_event = options['mode'].to_s == "merge" ? event.payload.dup : {} |
108 | 109 |
payload = perform_matching(event.payload) |
109 |
- options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, payload) } |
|
110 |
+ formatted_event.merge! interpolate_options(options['instructions'], payload) |
|
110 | 111 |
formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless options['skip_agent'].to_s == "true" |
111 | 112 |
formatted_event['created_at'] = event.created_at unless options['skip_created_at'].to_s == "true" |
112 | 113 |
create_event :payload => formatted_event |
@@ -161,7 +162,7 @@ module Agents |
||
161 | 162 |
re = Regexp.new(regexp) |
162 | 163 |
proc { |hash| |
163 | 164 |
mhash = {} |
164 |
- value = Utils.value_at(hash, path) |
|
165 |
+ value = interpolate_string(path, hash) |
|
165 | 166 |
if value.is_a?(String) && (m = re.match(value)) |
166 | 167 |
m.to_a.each_with_index { |s, i| |
167 | 168 |
mhash[i.to_s] = s |
@@ -1,6 +1,6 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class HipchatAgent < Agent |
3 |
- include JsonPathOptionsOverwritable |
|
3 |
+ include LiquidInterpolatable |
|
4 | 4 |
|
5 | 5 |
cannot_be_scheduled! |
6 | 6 |
cannot_create_events! |
@@ -18,22 +18,17 @@ module Agents |
||
18 | 18 |
If you want your message to notify the room members change `notify` to "true". |
19 | 19 |
Modify the background color of your message via the `color` attribute (one of "yellow", "red", "green", "purple", "gray", or "random") |
20 | 20 |
|
21 |
- If you want to specify either of those attributes per event, you can provide a [JSONPath](http://goessner.net/articles/JsonPath/) for each of them (except the `auth_token`). |
|
21 |
+ Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating. |
|
22 | 22 |
MD |
23 | 23 |
|
24 | 24 |
def default_options |
25 | 25 |
{ |
26 | 26 |
'auth_token' => '', |
27 | 27 |
'room_name' => '', |
28 |
- 'room_name_path' => '', |
|
29 | 28 |
'username' => "Huginn", |
30 |
- 'username_path' => '', |
|
31 | 29 |
'message' => "Hello from Huginn!", |
32 |
- 'message_path' => '', |
|
33 | 30 |
'notify' => false, |
34 |
- 'notify_path' => '', |
|
35 | 31 |
'color' => 'yellow', |
36 |
- 'color_path' => '', |
|
37 | 32 |
} |
38 | 33 |
end |
39 | 34 |
|
@@ -49,14 +44,9 @@ module Agents |
||
49 | 44 |
def receive(incoming_events) |
50 | 45 |
client = HipChat::Client.new(options[:auth_token]) |
51 | 46 |
incoming_events.each do |event| |
52 |
- mo = merge_json_path_options event |
|
47 |
+ mo = interpolate_options options, event.payload |
|
53 | 48 |
client[mo[:room_name]].send(mo[:username], mo[:message], :notify => mo[:notify].to_s == 'true' ? 1 : 0, :color => mo[:color]) |
54 | 49 |
end |
55 | 50 |
end |
56 |
- |
|
57 |
- private |
|
58 |
- def options_with_path |
|
59 |
- [:room_name, :username, :message, :notify, :color] |
|
60 |
- end |
|
61 | 51 |
end |
62 | 52 |
end |
@@ -2,6 +2,8 @@ require 'rturk' |
||
2 | 2 |
|
3 | 3 |
module Agents |
4 | 4 |
class HumanTaskAgent < Agent |
5 |
+ include LiquidInterpolatable |
|
6 |
+ |
|
5 | 7 |
default_schedule "every_10m" |
6 | 8 |
|
7 | 9 |
description <<-MD |
@@ -16,7 +18,7 @@ module Agents |
||
16 | 18 |
|
17 | 19 |
# Example |
18 | 20 |
|
19 |
- If created with an event, all HIT fields can contain interpolated values via [JSONPaths](http://goessner.net/articles/JsonPath/) placed between < and > characters. |
|
21 |
+ If created with an event, all HIT fields can contain interpolated values via [liquid templating](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid). |
|
20 | 22 |
For example, if the incoming event was a Twitter event, you could make a HITT to rate its sentiment like this: |
21 | 23 |
|
22 | 24 |
{ |
@@ -25,7 +27,7 @@ module Agents |
||
25 | 27 |
"hit": { |
26 | 28 |
"assignments": 1, |
27 | 29 |
"title": "Sentiment evaluation", |
28 |
- "description": "Please rate the sentiment of this message: '<$.message>'", |
|
30 |
+ "description": "Please rate the sentiment of this message: '{{message}}'", |
|
29 | 31 |
"reward": 0.05, |
30 | 32 |
"lifetime_in_seconds": "3600", |
31 | 33 |
"questions": [ |
@@ -83,7 +85,7 @@ module Agents |
||
83 | 85 |
"title": "Take a poll about some jokes", |
84 | 86 |
"instructions": "Please rank these jokes from most funny (5) to least funny (1)", |
85 | 87 |
"assignments": 3, |
86 |
- "row_template": "<$.joke>" |
|
88 |
+ "row_template": "{{joke}}" |
|
87 | 89 |
}, |
88 | 90 |
"hit": { |
89 | 91 |
"assignments": 5, |
@@ -168,7 +170,7 @@ module Agents |
||
168 | 170 |
{ |
169 | 171 |
'assignments' => 1, |
170 | 172 |
'title' => "Sentiment evaluation", |
171 |
- 'description' => "Please rate the sentiment of this message: '<$.message>'", |
|
173 |
+ 'description' => "Please rate the sentiment of this message: '{{message}}'", |
|
172 | 174 |
'reward' => 0.05, |
173 | 175 |
'lifetime_in_seconds' => 24 * 60 * 60, |
174 | 176 |
'questions' => |
@@ -332,7 +334,7 @@ module Agents |
||
332 | 334 |
'name' => "Item #{index + 1}", |
333 | 335 |
'key' => index, |
334 | 336 |
'required' => "true", |
335 |
- 'question' => Utils.interpolate_jsonpaths(options['poll_options']['row_template'], assignments[index].answers), |
|
337 |
+ 'question' => interpolate_string(options['poll_options']['row_template'], assignments[index].answers), |
|
336 | 338 |
'selections' => selections |
337 | 339 |
} |
338 | 340 |
end |
@@ -387,9 +389,9 @@ module Agents |
||
387 | 389 |
|
388 | 390 |
def create_hit(opts = {}) |
389 | 391 |
payload = opts['payload'] || {} |
390 |
- title = Utils.interpolate_jsonpaths(opts['title'], payload).strip |
|
391 |
- description = Utils.interpolate_jsonpaths(opts['description'], payload).strip |
|
392 |
- questions = Utils.recursively_interpolate_jsonpaths(opts['questions'], payload) |
|
392 |
+ title = interpolate_string(opts['title'], payload).strip |
|
393 |
+ description = interpolate_string(opts['description'], payload).strip |
|
394 |
+ questions = interpolate_options(opts['questions'], payload) |
|
393 | 395 |
hit = RTurk::Hit.create(:title => title) do |hit| |
394 | 396 |
hit.max_assignments = (opts['assignments'] || 1).to_i |
395 | 397 |
hit.description = description |
@@ -1,5 +1,7 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class JabberAgent < Agent |
3 |
+ include LiquidInterpolatable |
|
4 |
+ |
|
3 | 5 |
cannot_be_scheduled! |
4 | 6 |
cannot_create_events! |
5 | 7 |
|
@@ -10,7 +12,9 @@ module Agents |
||
10 | 12 |
|
11 | 13 |
The `message` is sent from `jabber_sender` to `jaber_receiver`. This message |
12 | 14 |
can contain any keys found in the source's payload, escaped using double curly braces. |
13 |
- ex: `"News Story: <$.title>: <$.url>"` |
|
15 |
+ ex: `"News Story: {{title}}: {{url}}"` |
|
16 |
+ |
|
17 |
+ Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating. |
|
14 | 18 |
MD |
15 | 19 |
|
16 | 20 |
def default_options |
@@ -20,7 +24,7 @@ module Agents |
||
20 | 24 |
'jabber_sender' => 'huginn@localhost', |
21 | 25 |
'jabber_receiver' => 'muninn@localhost', |
22 | 26 |
'jabber_password' => '', |
23 |
- 'message' => 'It will be <$.temp> out tomorrow', |
|
27 |
+ 'message' => 'It will be {{temp}} out tomorrow', |
|
24 | 28 |
'expected_receive_period_in_days' => "2" |
25 | 29 |
} |
26 | 30 |
end |
@@ -58,7 +62,7 @@ module Agents |
||
58 | 62 |
end |
59 | 63 |
|
60 | 64 |
def body(event) |
61 |
- Utils.interpolate_jsonpaths(options['message'], event.payload) |
|
65 |
+ interpolate_string(options['message'], event.payload) |
|
62 | 66 |
end |
63 | 67 |
end |
64 | 68 |
end |
@@ -2,10 +2,12 @@ require 'pp' |
||
2 | 2 |
|
3 | 3 |
module Agents |
4 | 4 |
class PeakDetectorAgent < Agent |
5 |
+ include LiquidInterpolatable |
|
6 |
+ |
|
5 | 7 |
cannot_be_scheduled! |
6 | 8 |
|
7 | 9 |
description <<-MD |
8 |
- Use a PeakDetectorAgent to watch for peaks in an event stream. When a peak is detected, the resulting Event will have a payload message of `message`. You can include extractions in the message, for example: `I saw a bar of: <foo.bar>` |
|
10 |
+ Use a PeakDetectorAgent to watch for peaks in an event stream. When a peak is detected, the resulting Event will have a payload message of `message`. You can include extractions in the message, for example: `I saw a bar of: {{foo.bar}}`, have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) for details. |
|
9 | 11 |
|
10 | 12 |
The `value_path` value is a [JSONPaths](http://goessner.net/articles/JsonPath/) to the value of interest. `group_by_path` is a hash path that will be used to group values, if present. |
11 | 13 |
|
@@ -67,7 +69,7 @@ module Agents |
||
67 | 69 |
if newest_value > average_value + std_multiple * standard_deviation |
68 | 70 |
memory['peaks'][group] << newest_time |
69 | 71 |
memory['peaks'][group].reject! { |p| p <= newest_time - window_duration } |
70 |
- create_event :payload => { 'message' => options['message'], 'peak' => newest_value, 'peak_time' => newest_time, 'grouped_by' => group.to_s } |
|
72 |
+ create_event :payload => { 'message' => interpolate_string(options['message'], event.payload), 'peak' => newest_value, 'peak_time' => newest_time, 'grouped_by' => group.to_s } |
|
71 | 73 |
end |
72 | 74 |
end |
73 | 75 |
end |
@@ -1,6 +1,6 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class PushbulletAgent < Agent |
3 |
- include JsonPathOptionsOverwritable |
|
3 |
+ include LiquidInterpolatable |
|
4 | 4 |
|
5 | 5 |
cannot_be_scheduled! |
6 | 6 |
cannot_create_events! |
@@ -20,7 +20,7 @@ module Agents |
||
20 | 20 |
|
21 | 21 |
You can provide a `title` and a `body`. |
22 | 22 |
|
23 |
- If you want to specify `title` or `body` per event, you can provide a [JSONPath](http://goessner.net/articles/JsonPath/) for each of them. |
|
23 |
+ In every value of the options hash you can use the liquid templating, learn more about it at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid). |
|
24 | 24 |
MD |
25 | 25 |
|
26 | 26 |
def default_options |
@@ -28,9 +28,7 @@ module Agents |
||
28 | 28 |
'api_key' => '', |
29 | 29 |
'device_id' => '', |
30 | 30 |
'title' => "Hello from Huginn!", |
31 |
- 'title_path' => '', |
|
32 |
- 'body' => '', |
|
33 |
- 'body_path' => '', |
|
31 |
+ 'body' => '{{body}}', |
|
34 | 32 |
} |
35 | 33 |
end |
36 | 34 |
|
@@ -52,16 +50,11 @@ module Agents |
||
52 | 50 |
|
53 | 51 |
private |
54 | 52 |
def query_options(event) |
55 |
- mo = merge_json_path_options event |
|
56 |
- basic_options.deep_merge(:body => {:title => mo[:title], :body => mo[:body]}) |
|
57 |
- end |
|
58 |
- |
|
59 |
- def basic_options |
|
60 |
- {:basic_auth => {:username =>options[:api_key], :password=>''}, :body => {:device_iden => options[:device_id], :type => 'note'}} |
|
61 |
- end |
|
62 |
- |
|
63 |
- def options_with_path |
|
64 |
- [:title, :body] |
|
53 |
+ mo = interpolate_options options, event.payload |
|
54 |
+ { |
|
55 |
+ :basic_auth => {:username =>mo[:api_key], :password=>''}, |
|
56 |
+ :body => {:device_iden => mo[:device_id], :title => mo[:title], :body => mo[:body], :type => 'note'} |
|
57 |
+ } |
|
65 | 58 |
end |
66 | 59 |
end |
67 | 60 |
end |
@@ -1,5 +1,6 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class TranslationAgent < Agent |
3 |
+ include LiquidInterpolatable |
|
3 | 4 |
|
4 | 5 |
cannot_be_scheduled! |
5 | 6 |
|
@@ -8,7 +9,7 @@ module Agents |
||
8 | 9 |
Services are provided using Microsoft Translator. You can [sign up](https://datamarket.azure.com/dataset/bing/microsofttranslator) and [register your application](https://datamarket.azure.com/developer/applications/register) to get `client_id` and `client_secret` which are required to use this agent. |
9 | 10 |
`to` must be filled with a [translator language code](http://msdn.microsoft.com/en-us/library/hh456380.aspx). |
10 | 11 |
|
11 |
- Specify what you would like to translate in `content` field, by specifying key and JSONPath of content to be translated. |
|
12 |
+ Specify what you would like to translate in `content` field, you can use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) specify which part of the payload you want to translate. |
|
12 | 13 |
|
13 | 14 |
`expected_receive_period_in_days` is the maximum number of days you would allow to pass between events. |
14 | 15 |
MD |
@@ -22,8 +23,8 @@ module Agents |
||
22 | 23 |
'to' => "fi", |
23 | 24 |
'expected_receive_period_in_days' => 1, |
24 | 25 |
'content' => { |
25 |
- 'text' => "$.message.text", |
|
26 |
- 'content' => "$.xyz" |
|
26 |
+ 'text' => "{{message.text}}", |
|
27 |
+ 'content' => "{{xyz}}" |
|
27 | 28 |
} |
28 | 29 |
} |
29 | 30 |
end |
@@ -68,8 +69,8 @@ module Agents |
||
68 | 69 |
incoming_events.each do |event| |
69 | 70 |
translated_event = {} |
70 | 71 |
options['content'].each_pair do |key, value| |
71 |
- to_be_translated = Utils.values_at event.payload, value |
|
72 |
- translated_event[key] = translate to_be_translated.first, options['to'], access_token |
|
72 |
+ to_be_translated = interpolate_string(value, event.payload) |
|
73 |
+ translated_event[key] = translate(to_be_translated.first, options['to'], access_token) |
|
73 | 74 |
end |
74 | 75 |
create_event :payload => translated_event |
75 | 76 |
end |
@@ -1,5 +1,7 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class TriggerAgent < Agent |
3 |
+ include LiquidInterpolatable |
|
4 |
+ |
|
3 | 5 |
cannot_be_scheduled! |
4 | 6 |
|
5 | 7 |
VALID_COMPARISON_TYPES = %w[regex !regex field<value field<=value field==value field!=value field>=value field>value] |
@@ -13,7 +15,7 @@ module Agents |
||
13 | 15 |
|
14 | 16 |
The `value` can be a single value or an array of values. In the case of an array, if one or more values match then the rule matches. |
15 | 17 |
|
16 |
- All rules must match for the Agent to match. The resulting Event will have a payload message of `message`. You can include extractions in the message, for example: `I saw a bar of: <foo.bar>` |
|
18 |
+ All rules must match for the Agent to match. The resulting Event will have a payload message of `message`. You can use liquid templating in the `message, have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) for details. |
|
17 | 19 |
|
18 | 20 |
Set `keep_event` to `true` if you'd like to re-emit the incoming event, optionally merged with 'message' when provided. |
19 | 21 |
|
@@ -46,7 +48,7 @@ module Agents |
||
46 | 48 |
'value' => "foo\\d+bar", |
47 | 49 |
'path' => "topkey.subkey.subkey.goal", |
48 | 50 |
}], |
49 |
- 'message' => "Looks like your pattern matched in '<value>'!" |
|
51 |
+ 'message' => "Looks like your pattern matched in '{{value}}'!" |
|
50 | 52 |
} |
51 | 53 |
end |
52 | 54 |
|
@@ -88,9 +90,9 @@ module Agents |
||
88 | 90 |
if match |
89 | 91 |
if keep_event? |
90 | 92 |
payload = event.payload.dup |
91 |
- payload['message'] = make_message(event[:payload]) if options['message'].present? |
|
93 |
+ payload['message'] = interpolate_string(options['message'], event.payload) if options['message'].present? |
|
92 | 94 |
else |
93 |
- payload = { 'message' => make_message(event[:payload]) } |
|
95 |
+ payload = { 'message' => interpolate_string(options['message'], event.payload) } |
|
94 | 96 |
end |
95 | 97 |
|
96 | 98 |
create_event :payload => payload |
@@ -3,6 +3,7 @@ require "twitter" |
||
3 | 3 |
module Agents |
4 | 4 |
class TwitterPublishAgent < Agent |
5 | 5 |
include TwitterConcern |
6 |
+ include LiquidInterpolatable |
|
6 | 7 |
|
7 | 8 |
cannot_be_scheduled! |
8 | 9 |
|
@@ -15,7 +16,7 @@ module Agents |
||
15 | 16 |
|
16 | 17 |
To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token). |
17 | 18 |
|
18 |
- You must also specify a `message_path` parameter: a [JSONPaths](http://goessner.net/articles/JsonPath/) to the value to tweet. |
|
19 |
+ You must also specify a `message` parameter, you can use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to format the message. |
|
19 | 20 |
|
20 | 21 |
Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent. |
21 | 22 |
MD |
@@ -31,7 +32,7 @@ module Agents |
||
31 | 32 |
def default_options |
32 | 33 |
{ |
33 | 34 |
'expected_update_period_in_days' => "10", |
34 |
- 'message_path' => "text" |
|
35 |
+ 'message' => "{{text}}" |
|
35 | 36 |
} |
36 | 37 |
end |
37 | 38 |
|
@@ -41,7 +42,7 @@ module Agents |
||
41 | 42 |
incoming_events = incoming_events.first(20) |
42 | 43 |
end |
43 | 44 |
incoming_events.each do |event| |
44 |
- tweet_text = Utils.value_at(event.payload, options['message_path']) |
|
45 |
+ tweet_text = interpolate_string(options['message'], event.payload) |
|
45 | 46 |
begin |
46 | 47 |
tweet = publish_tweet tweet_text |
47 | 48 |
create_event :payload => { |
@@ -0,0 +1,45 @@ |
||
1 |
+class MigrateAgentsToLiquidTemplating < ActiveRecord::Migration |
|
2 |
+ def up |
|
3 |
+ Agent.where(:type => 'Agents::HipchatAgent').each do |agent| |
|
4 |
+ LiquidMigrator.convert_all_agent_options(agent) |
|
5 |
+ end |
|
6 |
+ Agent.where(:type => 'Agents::EventFormattingAgent').each do |agent| |
|
7 |
+ agent.options['instructions'] = LiquidMigrator.convert_hash(agent.options['instructions'], {:merge_path_attributes => true, :leading_dollarsign_is_jsonpath => true}) |
|
8 |
+ agent.save |
|
9 |
+ end |
|
10 |
+ Agent.where(:type => 'Agents::PushbulletAgent').each do |agent| |
|
11 |
+ LiquidMigrator.convert_all_agent_options(agent) |
|
12 |
+ end |
|
13 |
+ Agent.where(:type => 'Agents::JabberAgent').each do |agent| |
|
14 |
+ LiquidMigrator.convert_all_agent_options(agent) |
|
15 |
+ end |
|
16 |
+ Agent.where(:type => 'Agents::DataOutputAgent').each do |agent| |
|
17 |
+ LiquidMigrator.convert_all_agent_options(agent) |
|
18 |
+ end |
|
19 |
+ Agent.where(:type => 'Agents::TranslationAgent').each do |agent| |
|
20 |
+ agent.options['content'] = LiquidMigrator.convert_hash(agent.options['content'], {:merge_path_attributes => true, :leading_dollarsign_is_jsonpath => true}) |
|
21 |
+ agent.save |
|
22 |
+ end |
|
23 |
+ Agent.where(:type => 'Agents::TwitterPublishAgent').each do |agent| |
|
24 |
+ if (message = agent.options.delete('message_path')).present? |
|
25 |
+ agent.options['message'] = "{{#{message}}}" |
|
26 |
+ agent.save |
|
27 |
+ end |
|
28 |
+ end |
|
29 |
+ Agent.where(:type => 'Agents::TriggerAgent').each do |agent| |
|
30 |
+ agent.options['message'] = LiquidMigrator.convert_make_message(agent.options['message']) |
|
31 |
+ agent.save |
|
32 |
+ end |
|
33 |
+ Agent.where(:type => 'Agents::PeakDetectorAgent').each do |agent| |
|
34 |
+ agent.options['message'] = LiquidMigrator.convert_make_message(agent.options['message']) |
|
35 |
+ agent.save |
|
36 |
+ end |
|
37 |
+ Agent.where(:type => 'Agents::HumanTaskAgent').each do |agent| |
|
38 |
+ LiquidMigrator.convert_all_agent_options(agent) |
|
39 |
+ end |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ def down |
|
43 |
+ raise ActiveRecord::IrreversibleMigration, "Cannot revert migration to Liquid templating" |
|
44 |
+ end |
|
45 |
+end |
@@ -0,0 +1,78 @@ |
||
1 |
+module LiquidMigrator |
|
2 |
+ def self.convert_all_agent_options(agent) |
|
3 |
+ agent.options = self.convert_hash(agent.options, {:merge_path_attributes => true, :leading_dollarsign_is_jsonpath => true}) |
|
4 |
+ agent.save! |
|
5 |
+ end |
|
6 |
+ |
|
7 |
+ def self.convert_hash(hash, options={}) |
|
8 |
+ options = {:merge_path_attributes => false, :leading_dollarsign_is_jsonpath => false}.merge options |
|
9 |
+ keys_to_remove = [] |
|
10 |
+ hash.tap do |hash| |
|
11 |
+ hash.each_pair do |key, value| |
|
12 |
+ case value.class.to_s |
|
13 |
+ when 'String', 'FalseClass', 'TrueClass' |
|
14 |
+ path_key = "#{key}_path" |
|
15 |
+ if options[:merge_path_attributes] && !hash[path_key].nil? |
|
16 |
+ # replace the value if the path is present |
|
17 |
+ value = hash[path_key] if hash[path_key].present? |
|
18 |
+ # in any case delete the path attibute |
|
19 |
+ keys_to_remove << path_key |
|
20 |
+ end |
|
21 |
+ hash[key] = LiquidMigrator.convert_string value, options[:leading_dollarsign_is_jsonpath] |
|
22 |
+ when 'ActiveSupport::HashWithIndifferentAccess' |
|
23 |
+ hash[key] = convert_hash(hash[key], options) |
|
24 |
+ when 'Array' |
|
25 |
+ hash[key] = hash[key].collect { |k| |
|
26 |
+ if k.class == String |
|
27 |
+ convert_string(k, options[:leading_dollarsign_is_jsonpath]) |
|
28 |
+ else |
|
29 |
+ convert_hash(k, options) |
|
30 |
+ end |
|
31 |
+ } |
|
32 |
+ end |
|
33 |
+ end |
|
34 |
+ # remove the unneeded *_path attributes |
|
35 |
+ end.select { |k, v| !keys_to_remove.include? k } |
|
36 |
+ end |
|
37 |
+ |
|
38 |
+ def self.convert_string(string, leading_dollarsign_is_jsonpath=false) |
|
39 |
+ if string == true || string == false |
|
40 |
+ # there might be empty *_path attributes for boolean defaults |
|
41 |
+ string |
|
42 |
+ elsif string[0] == '$' && leading_dollarsign_is_jsonpath |
|
43 |
+ # in most cases a *_path attribute |
|
44 |
+ convert_json_path string |
|
45 |
+ else |
|
46 |
+ # migrate the old interpolation syntax to the new liquid based |
|
47 |
+ string.gsub(/<([^>]+)>/).each do |
|
48 |
+ match = $1 |
|
49 |
+ if match =~ /\Aescape / |
|
50 |
+ # convert the old escape syntax to a liquid filter |
|
51 |
+ self.convert_json_path(match.gsub(/\Aescape /, '').strip, ' | uri_escape') |
|
52 |
+ else |
|
53 |
+ self.convert_json_path(match.strip) |
|
54 |
+ end |
|
55 |
+ end |
|
56 |
+ end |
|
57 |
+ end |
|
58 |
+ |
|
59 |
+ def self.convert_make_message(string) |
|
60 |
+ string.gsub(/<([^>]+)>/, "{{\\1}}") |
|
61 |
+ end |
|
62 |
+ |
|
63 |
+ def self.convert_json_path(string, filter = "") |
|
64 |
+ check_path(string) |
|
65 |
+ if string.start_with? '$.' |
|
66 |
+ "{{#{string[2..-1]}#{filter}}}" |
|
67 |
+ else |
|
68 |
+ "{{#{string[1..-1]}#{filter}}}" |
|
69 |
+ end |
|
70 |
+ end |
|
71 |
+ |
|
72 |
+ def self.check_path(string) |
|
73 |
+ if string !~ /\A(\$\.?)?(\w+\.)*(\w+)\Z/ |
|
74 |
+ raise "JSONPath '#{string}' is too complex, please check your migration." |
|
75 |
+ end |
|
76 |
+ end |
|
77 |
+end |
|
78 |
+ |
@@ -0,0 +1,156 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe LiquidMigrator do |
|
4 |
+ describe "converting JSONPath strings" do |
|
5 |
+ it "should work" do |
|
6 |
+ LiquidMigrator.convert_string("$.data", true).should == "{{data}}" |
|
7 |
+ LiquidMigrator.convert_string("$.data.test", true).should == "{{data.test}}" |
|
8 |
+ LiquidMigrator.convert_string("$first_title", true).should == "{{first_title}}" |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ it "should ignore strings which just contain a JSONPath" do |
|
12 |
+ LiquidMigrator.convert_string("$.data").should == "$.data" |
|
13 |
+ LiquidMigrator.convert_string("$first_title").should == "$first_title" |
|
14 |
+ LiquidMigrator.convert_string(" $.data", true).should == " $.data" |
|
15 |
+ LiquidMigrator.convert_string("lorem $.data", true).should == "lorem $.data" |
|
16 |
+ end |
|
17 |
+ it "should raise an exception when encountering complex JSONPaths" do |
|
18 |
+ expect { LiquidMigrator.convert_string("$.data.test.*", true) }. |
|
19 |
+ to raise_error("JSONPath '$.data.test.*' is too complex, please check your migration.") |
|
20 |
+ end |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ describe "converting escaped JSONPath strings" do |
|
24 |
+ it "should work" do |
|
25 |
+ LiquidMigrator.convert_string("Weather looks like <$.conditions> according to the forecast at <$.pretty_date.time>").should == |
|
26 |
+ "Weather looks like {{conditions}} according to the forecast at {{pretty_date.time}}" |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ it "should convert the 'escape' method correctly" do |
|
30 |
+ LiquidMigrator.convert_string("Escaped: <escape $.content.name>\nNot escaped: <$.content.name>").should == |
|
31 |
+ "Escaped: {{content.name | uri_escape}}\nNot escaped: {{content.name}}" |
|
32 |
+ end |
|
33 |
+ |
|
34 |
+ it "should raise an exception when encountering complex JSONPaths" do |
|
35 |
+ expect { LiquidMigrator.convert_string("Received <$.content.text.*> from <$.content.name> .") }. |
|
36 |
+ to raise_error("JSONPath '$.content.text.*' is too complex, please check your migration.") |
|
37 |
+ end |
|
38 |
+ end |
|
39 |
+ |
|
40 |
+ describe "migrating a hash" do |
|
41 |
+ it "should convert every attribute" do |
|
42 |
+ LiquidMigrator.convert_hash({'a' => "$.data", 'b' => "This is a <$.test>"}).should == |
|
43 |
+ {'a' => "$.data", 'b' => "This is a {{test}}"} |
|
44 |
+ end |
|
45 |
+ it "should work with leading_dollarsign_is_jsonpath" do |
|
46 |
+ LiquidMigrator.convert_hash({'a' => "$.data", 'b' => "This is a <$.test>"}, leading_dollarsign_is_jsonpath: true).should == |
|
47 |
+ {'a' => "{{data}}", 'b' => "This is a {{test}}"} |
|
48 |
+ end |
|
49 |
+ it "should use the corresponding *_path attributes when using merge_path_attributes"do |
|
50 |
+ LiquidMigrator.convert_hash({'a' => "default", 'a_path' => "$.data"}, {leading_dollarsign_is_jsonpath: true, merge_path_attributes: true}).should == |
|
51 |
+ {'a' => "{{data}}"} |
|
52 |
+ end |
|
53 |
+ it "should raise an exception when encountering complex JSONPaths" do |
|
54 |
+ expect { LiquidMigrator.convert_hash({'b' => "This is <$.complex[2]>"}) }. |
|
55 |
+ to raise_error("JSONPath '$.complex[2]' is too complex, please check your migration.") |
|
56 |
+ end |
|
57 |
+ end |
|
58 |
+ |
|
59 |
+ describe "migrating the 'make_message' format" do |
|
60 |
+ it "should work" do |
|
61 |
+ LiquidMigrator.convert_make_message('<message>').should == '{{message}}' |
|
62 |
+ LiquidMigrator.convert_make_message('<new.message>').should == '{{new.message}}' |
|
63 |
+ LiquidMigrator.convert_make_message('Hello <world>. How is <nested.life>').should == 'Hello {{world}}. How is {{nested.life}}' |
|
64 |
+ end |
|
65 |
+ end |
|
66 |
+ |
|
67 |
+ describe "migrating an actual agent" do |
|
68 |
+ before do |
|
69 |
+ valid_params = { |
|
70 |
+ 'auth_token' => 'token', |
|
71 |
+ 'room_name' => 'test', |
|
72 |
+ 'room_name_path' => '', |
|
73 |
+ 'username' => "Huginn", |
|
74 |
+ 'username_path' => '$.username', |
|
75 |
+ 'message' => "Hello from Huginn!", |
|
76 |
+ 'message_path' => '$.message', |
|
77 |
+ 'notify' => false, |
|
78 |
+ 'notify_path' => '', |
|
79 |
+ 'color' => 'yellow', |
|
80 |
+ 'color_path' => '', |
|
81 |
+ } |
|
82 |
+ |
|
83 |
+ @agent = Agents::HipchatAgent.new(:name => "somename", :options => valid_params) |
|
84 |
+ @agent.user = users(:jane) |
|
85 |
+ @agent.save! |
|
86 |
+ end |
|
87 |
+ |
|
88 |
+ it "should work" do |
|
89 |
+ LiquidMigrator.convert_all_agent_options(@agent) |
|
90 |
+ @agent.reload.options.should == {"auth_token" => 'token', 'color' => 'yellow', 'notify' => false, 'room_name' => 'test', 'username' => '{{username}}', 'message' => '{{message}}'} |
|
91 |
+ end |
|
92 |
+ |
|
93 |
+ it "should work with nested hashes" do |
|
94 |
+ @agent.options['very'] = {'nested' => '$.value'} |
|
95 |
+ LiquidMigrator.convert_all_agent_options(@agent) |
|
96 |
+ @agent.reload.options.should == {"auth_token" => 'token', 'color' => 'yellow', 'very' => {'nested' => '{{value}}'}, 'notify' => false, 'room_name' => 'test', 'username' => '{{username}}', 'message' => '{{message}}'} |
|
97 |
+ end |
|
98 |
+ |
|
99 |
+ it "should work with nested arrays" do |
|
100 |
+ @agent.options['array'] = ["one", "$.two"] |
|
101 |
+ LiquidMigrator.convert_all_agent_options(@agent) |
|
102 |
+ @agent.reload.options.should == {"auth_token" => 'token', 'color' => 'yellow', 'array' => ['one', '{{two}}'], 'notify' => false, 'room_name' => 'test', 'username' => '{{username}}', 'message' => '{{message}}'} |
|
103 |
+ end |
|
104 |
+ |
|
105 |
+ it "should raise an exception when encountering complex JSONPaths" do |
|
106 |
+ @agent.options['username_path'] = "$.very.complex[*]" |
|
107 |
+ expect { LiquidMigrator.convert_all_agent_options(@agent) }. |
|
108 |
+ to raise_error("JSONPath '$.very.complex[*]' is too complex, please check your migration.") |
|
109 |
+ end |
|
110 |
+ |
|
111 |
+ it "should work with the human task agent" do |
|
112 |
+ valid_params = { |
|
113 |
+ 'expected_receive_period_in_days' => 2, |
|
114 |
+ 'trigger_on' => "event", |
|
115 |
+ 'hit' => |
|
116 |
+ { |
|
117 |
+ 'assignments' => 1, |
|
118 |
+ 'title' => "Sentiment evaluation", |
|
119 |
+ 'description' => "Please rate the sentiment of this message: '<$.message>'", |
|
120 |
+ 'reward' => 0.05, |
|
121 |
+ 'lifetime_in_seconds' => 24 * 60 * 60, |
|
122 |
+ 'questions' => |
|
123 |
+ [ |
|
124 |
+ { |
|
125 |
+ 'type' => "selection", |
|
126 |
+ 'key' => "sentiment", |
|
127 |
+ 'name' => "Sentiment", |
|
128 |
+ 'required' => "true", |
|
129 |
+ 'question' => "Please select the best sentiment value:", |
|
130 |
+ 'selections' => |
|
131 |
+ [ |
|
132 |
+ { 'key' => "happy", 'text' => "Happy" }, |
|
133 |
+ { 'key' => "sad", 'text' => "Sad" }, |
|
134 |
+ { 'key' => "neutral", 'text' => "Neutral" } |
|
135 |
+ ] |
|
136 |
+ }, |
|
137 |
+ { |
|
138 |
+ 'type' => "free_text", |
|
139 |
+ 'key' => "feedback", |
|
140 |
+ 'name' => "Have any feedback for us?", |
|
141 |
+ 'required' => "false", |
|
142 |
+ 'question' => "Feedback", |
|
143 |
+ 'default' => "Type here...", |
|
144 |
+ 'min_length' => "2", |
|
145 |
+ 'max_length' => "2000" |
|
146 |
+ } |
|
147 |
+ ] |
|
148 |
+ } |
|
149 |
+ } |
|
150 |
+ @agent = Agents::HumanTaskAgent.new(:name => "somename", :options => valid_params) |
|
151 |
+ @agent.user = users(:jane) |
|
152 |
+ LiquidMigrator.convert_all_agent_options(@agent) |
|
153 |
+ @agent.reload.options['hit']['description'].should == "Please rate the sentiment of this message: '{{message}}'" |
|
154 |
+ end |
|
155 |
+ end |
|
156 |
+end |
@@ -1,8 +1,11 @@ |
||
1 | 1 |
# encoding: utf-8 |
2 | 2 |
|
3 | 3 |
require 'spec_helper' |
4 |
+require 'models/concerns/liquid_interpolatable' |
|
4 | 5 |
|
5 | 6 |
describe Agents::DataOutputAgent do |
7 |
+ it_behaves_like LiquidInterpolatable |
|
8 |
+ |
|
6 | 9 |
let(:agent) do |
7 | 10 |
_agent = Agents::DataOutputAgent.new(:name => 'My Data Output Agent') |
8 | 11 |
_agent.options = _agent.default_options.merge('secrets' => ['secret1', 'secret2'], 'events_to_show' => 2) |
@@ -6,13 +6,13 @@ describe Agents::EventFormattingAgent do |
||
6 | 6 |
:name => "somename", |
7 | 7 |
:options => { |
8 | 8 |
:instructions => { |
9 |
- :message => "Received <$.content.text.*> from <$.content.name> .", |
|
10 |
- :subject => "Weather looks like <$.conditions> according to the forecast at <$.pretty_date.time>" |
|
9 |
+ :message => "Received {{content.text}} from {{content.name}} .", |
|
10 |
+ :subject => "Weather looks like {{conditions}} according to the forecast at {{pretty_date.time}}" |
|
11 | 11 |
}, |
12 | 12 |
:mode => "clean", |
13 | 13 |
:matchers => [ |
14 | 14 |
{ |
15 |
- :path => "$.date.pretty", |
|
15 |
+ :path => "{{date.pretty}}", |
|
16 | 16 |
:regexp => "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)", |
17 | 17 |
:to => "pretty_date", |
18 | 18 |
}, |
@@ -82,7 +82,7 @@ describe Agents::EventFormattingAgent do |
||
82 | 82 |
it "should allow escaping" do |
83 | 83 |
@event.payload[:content][:name] = "escape this!?" |
84 | 84 |
@event.save! |
85 |
- @checker.options[:instructions][:message] = "Escaped: <escape $.content.name>\nNot escaped: <$.content.name>" |
|
85 |
+ @checker.options[:instructions][:message] = "Escaped: {{content.name | uri_escape}}\nNot escaped: {{content.name}}" |
|
86 | 86 |
@checker.save! |
87 | 87 |
@checker.receive([@event]) |
88 | 88 |
Event.last.payload[:message].should == "Escaped: escape+this%21%3F\nNot escaped: escape this!?" |
@@ -1,22 +1,17 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/json_path_options_overwritable' |
|
2 |
+require 'models/concerns/liquid_interpolatable' |
|
3 | 3 |
|
4 | 4 |
describe Agents::HipchatAgent do |
5 |
- it_behaves_like JsonPathOptionsOverwritable |
|
5 |
+ it_behaves_like LiquidInterpolatable |
|
6 | 6 |
|
7 | 7 |
before(:each) do |
8 | 8 |
@valid_params = { |
9 | 9 |
'auth_token' => 'token', |
10 | 10 |
'room_name' => 'test', |
11 |
- 'room_name_path' => '', |
|
12 |
- 'username' => "Huginn", |
|
13 |
- 'username_path' => '$.username', |
|
14 |
- 'message' => "Hello from Huginn!", |
|
15 |
- 'message_path' => '$.message', |
|
11 |
+ 'username' => "{{username}}", |
|
12 |
+ 'message' => "{{message}}", |
|
16 | 13 |
'notify' => false, |
17 |
- 'notify_path' => '', |
|
18 | 14 |
'color' => 'yellow', |
19 |
- 'color_path' => '', |
|
20 | 15 |
} |
21 | 16 |
|
22 | 17 |
@checker = Agents::HipchatAgent.new(:name => "somename", :options => @valid_params) |
@@ -1,6 +1,9 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
+require 'models/concerns/liquid_interpolatable' |
|
2 | 3 |
|
3 | 4 |
describe Agents::HumanTaskAgent do |
5 |
+ it_behaves_like LiquidInterpolatable |
|
6 |
+ |
|
4 | 7 |
before do |
5 | 8 |
@checker = Agents::HumanTaskAgent.new(:name => "my human task agent") |
6 | 9 |
@checker.options = @checker.default_options |
@@ -116,19 +119,19 @@ describe Agents::HumanTaskAgent do |
||
116 | 119 |
@checker.options['poll_options'] = { 'title' => "Take a poll about jokes", |
117 | 120 |
'instructions' => "Rank these by how funny they are", |
118 | 121 |
'assignments' => 3, |
119 |
- 'row_template' => "<$.joke>" } |
|
122 |
+ 'row_template' => "{{joke}}" } |
|
120 | 123 |
@checker.should be_valid |
121 | 124 |
@checker.options['poll_options'] = { 'instructions' => "Rank these by how funny they are", |
122 | 125 |
'assignments' => 3, |
123 |
- 'row_template' => "<$.joke>" } |
|
126 |
+ 'row_template' => "{{joke}}" } |
|
124 | 127 |
@checker.should_not be_valid |
125 | 128 |
@checker.options['poll_options'] = { 'title' => "Take a poll about jokes", |
126 | 129 |
'assignments' => 3, |
127 |
- 'row_template' => "<$.joke>" } |
|
130 |
+ 'row_template' => "{{joke}}" } |
|
128 | 131 |
@checker.should_not be_valid |
129 | 132 |
@checker.options['poll_options'] = { 'title' => "Take a poll about jokes", |
130 | 133 |
'instructions' => "Rank these by how funny they are", |
131 |
- 'row_template' => "<$.joke>" } |
|
134 |
+ 'row_template' => "{{joke}}" } |
|
132 | 135 |
@checker.should_not be_valid |
133 | 136 |
@checker.options['poll_options'] = { 'title' => "Take a poll about jokes", |
134 | 137 |
'instructions' => "Rank these by how funny they are", |
@@ -207,9 +210,9 @@ describe Agents::HumanTaskAgent do |
||
207 | 210 |
|
208 | 211 |
describe "creating hits" do |
209 | 212 |
it "can create HITs based on events, interpolating their values" do |
210 |
- @checker.options['hit']['title'] = "Hi <.name>" |
|
211 |
- @checker.options['hit']['description'] = "Make something for <.name>" |
|
212 |
- @checker.options['hit']['questions'][0]['name'] = "<.name> Question 1" |
|
213 |
+ @checker.options['hit']['title'] = "Hi {{name}}" |
|
214 |
+ @checker.options['hit']['description'] = "Make something for {{name}}" |
|
215 |
+ @checker.options['hit']['questions'][0]['name'] = "{{name}} Question 1" |
|
213 | 216 |
|
214 | 217 |
question_form = nil |
215 | 218 |
hitInterface = OpenStruct.new |
@@ -232,7 +235,7 @@ describe Agents::HumanTaskAgent do |
||
232 | 235 |
end |
233 | 236 |
|
234 | 237 |
it "works without an event too" do |
235 |
- @checker.options['hit']['title'] = "Hi <.name>" |
|
238 |
+ @checker.options['hit']['title'] = "Hi {{name}}" |
|
236 | 239 |
hitInterface = OpenStruct.new |
237 | 240 |
hitInterface.id = 123 |
238 | 241 |
mock(hitInterface).question_form(instance_of Agents::HumanTaskAgent::AgentQuestionForm) |
@@ -483,7 +486,7 @@ describe Agents::HumanTaskAgent do |
||
483 | 486 |
'title' => "Hi!", |
484 | 487 |
'instructions' => "hello!", |
485 | 488 |
'assignments' => 2, |
486 |
- 'row_template' => "This is <.sentiment>" |
|
489 |
+ 'row_template' => "This is {{sentiment}}" |
|
487 | 490 |
} |
488 | 491 |
@event.save! |
489 | 492 |
mock(RTurk::GetReviewableHITs).create { mock!.hit_ids { %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345] } } |
@@ -1,6 +1,9 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
+require 'models/concerns/liquid_interpolatable' |
|
2 | 3 |
|
3 | 4 |
describe Agents::JabberAgent do |
5 |
+ it_behaves_like LiquidInterpolatable |
|
6 |
+ |
|
4 | 7 |
let(:sent) { [] } |
5 | 8 |
let(:config) { |
6 | 9 |
{ |
@@ -9,7 +12,7 @@ describe Agents::JabberAgent do |
||
9 | 12 |
jabber_sender: 'foo@localhost', |
10 | 13 |
jabber_receiver: 'bar@localhost', |
11 | 14 |
jabber_password: 'password', |
12 |
- message: 'Warning! <$.title> - <$.url>', |
|
15 |
+ message: 'Warning! {{title}} - {{url}}', |
|
13 | 16 |
expected_receive_period_in_days: '2' |
14 | 17 |
} |
15 | 18 |
} |
@@ -1,6 +1,9 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
+require 'models/concerns/liquid_interpolatable' |
|
2 | 3 |
|
3 | 4 |
describe Agents::PeakDetectorAgent do |
5 |
+ it_behaves_like LiquidInterpolatable |
|
6 |
+ |
|
4 | 7 |
before do |
5 | 8 |
@valid_params = { |
6 | 9 |
'name' => "my peak detector agent", |
@@ -1,14 +1,14 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/json_path_options_overwritable' |
|
2 |
+require 'models/concerns/liquid_interpolatable' |
|
3 | 3 |
|
4 | 4 |
describe Agents::PushbulletAgent do |
5 |
- it_behaves_like JsonPathOptionsOverwritable |
|
5 |
+ it_behaves_like LiquidInterpolatable |
|
6 | 6 |
|
7 | 7 |
before(:each) do |
8 | 8 |
@valid_params = { |
9 | 9 |
'api_key' => 'token', |
10 | 10 |
'device_id' => '124', |
11 |
- 'body_path' => '$.body', |
|
11 |
+ 'body' => '{{body}}', |
|
12 | 12 |
'title' => 'hello from huginn' |
13 | 13 |
} |
14 | 14 |
|
@@ -39,23 +39,18 @@ describe Agents::PushbulletAgent do |
||
39 | 39 |
end |
40 | 40 |
|
41 | 41 |
describe "helpers" do |
42 |
- it "it should return the correct basic_options" do |
|
43 |
- @checker.send(:basic_options).should == {:basic_auth => {:username =>@checker.options[:api_key], :password=>''}, |
|
44 |
- :body => {:device_iden => @checker.options[:device_id], :type => 'note'}} |
|
45 |
- end |
|
46 |
- |
|
47 |
- |
|
48 | 42 |
it "should return the query_options" do |
49 |
- @checker.send(:query_options, @event).should == @checker.send(:basic_options).deep_merge({ |
|
50 |
- :body => {:title => 'hello from huginn', :body => 'One two test'} |
|
51 |
- }) |
|
43 |
+ @checker.send(:query_options, @event).should == { |
|
44 |
+ :body => {:title => 'hello from huginn', :body => 'One two test', :device_iden => @checker.options[:device_id], :type => 'note'}, |
|
45 |
+ :basic_auth => {:username =>@checker.options[:api_key], :password=>''} |
|
46 |
+ } |
|
52 | 47 |
end |
53 | 48 |
end |
54 | 49 |
|
55 | 50 |
describe "#receive" do |
56 | 51 |
it "send a message to the hipchat" do |
57 | 52 |
stub_request(:post, "https://token:@api.pushbullet.com/api/pushes"). |
58 |
- with(:body => "device_iden=124&type=note&title=hello%20from%20huginn&body=One%20two%20test"). |
|
53 |
+ with(:body => "device_iden=124&title=hello%20from%20huginn&body=One%20two%20test&type=note"). |
|
59 | 54 |
to_return(:status => 200, :body => "ok", :headers => {}) |
60 | 55 |
dont_allow(@checker).error |
61 | 56 |
@checker.receive([@event]) |
@@ -63,7 +58,7 @@ describe Agents::PushbulletAgent do |
||
63 | 58 |
|
64 | 59 |
it "should log resquests which return an error" do |
65 | 60 |
stub_request(:post, "https://token:@api.pushbullet.com/api/pushes"). |
66 |
- with(:body => "device_iden=124&type=note&title=hello%20from%20huginn&body=One%20two%20test"). |
|
61 |
+ with(:body => "device_iden=124&title=hello%20from%20huginn&body=One%20two%20test&type=note"). |
|
67 | 62 |
to_return(:status => 200, :body => "error", :headers => {}) |
68 | 63 |
mock(@checker).error("error") |
69 | 64 |
@checker.receive([@event]) |
@@ -1,6 +1,10 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
+require 'models/concerns/liquid_interpolatable' |
|
3 |
+ |
|
2 | 4 |
|
3 | 5 |
describe Agents::TranslationAgent do |
6 |
+ it_behaves_like LiquidInterpolatable |
|
7 |
+ |
|
4 | 8 |
before do |
5 | 9 |
@valid_params = { |
6 | 10 |
:name => "somename", |
@@ -10,8 +14,8 @@ describe Agents::TranslationAgent do |
||
10 | 14 |
:to => "fi", |
11 | 15 |
:expected_receive_period_in_days => 1, |
12 | 16 |
:content => { |
13 |
- :text => "$.message", |
|
14 |
- :content => "$.xyz" |
|
17 |
+ :text => "{{message}}", |
|
18 |
+ :content => "{{xyz}}" |
|
15 | 19 |
} |
16 | 20 |
} |
17 | 21 |
} |
@@ -1,6 +1,9 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
+require 'models/concerns/liquid_interpolatable' |
|
2 | 3 |
|
3 | 4 |
describe Agents::TriggerAgent do |
5 |
+ it_behaves_like LiquidInterpolatable |
|
6 |
+ |
|
4 | 7 |
before do |
5 | 8 |
@valid_params = { |
6 | 9 |
'name' => "my trigger agent", |
@@ -11,7 +14,7 @@ describe Agents::TriggerAgent do |
||
11 | 14 |
'value' => "a\\db", |
12 | 15 |
'path' => "foo.bar.baz", |
13 | 16 |
}], |
14 |
- 'message' => "I saw '<foo.bar.baz>' from <name>" |
|
17 |
+ 'message' => "I saw '{{foo.bar.baz}}' from {{name}}" |
|
15 | 18 |
} |
16 | 19 |
} |
17 | 20 |
|
@@ -9,7 +9,7 @@ describe Agents::TwitterPublishAgent do |
||
9 | 9 |
:consumer_secret => "---", |
10 | 10 |
:oauth_token => "---", |
11 | 11 |
:oauth_token_secret => "---", |
12 |
- :message_path => "text" |
|
12 |
+ :message => "{{text}}" |
|
13 | 13 |
} |
14 | 14 |
|
15 | 15 |
@checker = Agents::TwitterPublishAgent.new(:name => "HuginnBot", :options => @opts) |
@@ -1,31 +0,0 @@ |
||
1 |
-require 'spec_helper' |
|
2 |
- |
|
3 |
-shared_examples_for JsonPathOptionsOverwritable do |
|
4 |
- before(:each) do |
|
5 |
- @valid_params = described_class.new.default_options |
|
6 |
- |
|
7 |
- @checker = described_class.new(:name => "somename", :options => @valid_params) |
|
8 |
- @checker.user = users(:jane) |
|
9 |
- |
|
10 |
- @event = Event.new |
|
11 |
- @event.agent = agents(:bob_weather_agent) |
|
12 |
- @event.payload = { :room_name => 'test room', :message => 'Looks like its going to rain', username: "Huggin user"} |
|
13 |
- @event.save! |
|
14 |
- end |
|
15 |
- |
|
16 |
- describe "select_option" do |
|
17 |
- it "should use the room_name_path if specified" do |
|
18 |
- @checker.options['room_name_path'] = "$.room_name" |
|
19 |
- @checker.send(:select_option, @event, :room_name).should == "test room" |
|
20 |
- end |
|
21 |
- |
|
22 |
- it "should use the normal option when the path option is blank" do |
|
23 |
- @checker.options['room_name'] = 'test' |
|
24 |
- @checker.send(:select_option, @event, :room_name).should == "test" |
|
25 |
- end |
|
26 |
- end |
|
27 |
- |
|
28 |
- it "should merge all options" do |
|
29 |
- @checker.send(:merge_json_path_options, @event).symbolize_keys.keys.should == @checker.send(:options_with_path) |
|
30 |
- end |
|
31 |
-end |
@@ -0,0 +1,56 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+shared_examples_for LiquidInterpolatable do |
|
4 |
+ before(:each) do |
|
5 |
+ @valid_params = { |
|
6 |
+ "normal" => "just some normal text", |
|
7 |
+ "variable" => "{{variable}}", |
|
8 |
+ "text" => "Some test with an embedded {{variable}}", |
|
9 |
+ "escape" => "This should be {{hello_world | uri_escape}}" |
|
10 |
+ } |
|
11 |
+ |
|
12 |
+ @checker = described_class.new(:name => "somename", :options => @valid_params) |
|
13 |
+ @checker.user = users(:jane) |
|
14 |
+ |
|
15 |
+ @event = Event.new |
|
16 |
+ @event.agent = agents(:bob_weather_agent) |
|
17 |
+ @event.payload = { :variable => 'hello', :hello_world => "Hello world"} |
|
18 |
+ @event.save! |
|
19 |
+ end |
|
20 |
+ |
|
21 |
+ describe "interpolating liquid templates" do |
|
22 |
+ it "should work" do |
|
23 |
+ @checker.interpolate_options(@checker.options, @event.payload).should == { |
|
24 |
+ "normal" => "just some normal text", |
|
25 |
+ "variable" => "hello", |
|
26 |
+ "text" => "Some test with an embedded hello", |
|
27 |
+ "escape" => "This should be Hello+world" |
|
28 |
+ } |
|
29 |
+ end |
|
30 |
+ |
|
31 |
+ it "hsould work with arrays", focus: true do |
|
32 |
+ @checker.options = {"value" => ["{{variable}}", "Much array", "Hey, {{hello_world}}"]} |
|
33 |
+ @checker.interpolate_options(@checker.options, @event.payload).should == { |
|
34 |
+ "value" => ["hello", "Much array", "Hey, Hello world"] |
|
35 |
+ } |
|
36 |
+ end |
|
37 |
+ |
|
38 |
+ it "should work recursively" do |
|
39 |
+ @checker.options['hash'] = {'recursive' => "{{variable}}"} |
|
40 |
+ @checker.options['indifferent_hash'] = ActiveSupport::HashWithIndifferentAccess.new({'recursive' => "{{variable}}"}) |
|
41 |
+ @checker.interpolate_options(@checker.options, @event.payload).should == { |
|
42 |
+ "normal" => "just some normal text", |
|
43 |
+ "variable" => "hello", |
|
44 |
+ "text" => "Some test with an embedded hello", |
|
45 |
+ "escape" => "This should be Hello+world", |
|
46 |
+ "hash" => {'recursive' => 'hello'}, |
|
47 |
+ "indifferent_hash" => {'recursive' => 'hello'}, |
|
48 |
+ } |
|
49 |
+ end |
|
50 |
+ |
|
51 |
+ it "should work for strings" do |
|
52 |
+ @checker.send(:interpolate_string, "{{variable}}", @event.payload).should == "hello" |
|
53 |
+ @checker.send(:interpolate_string, "{{variable}} you", @event.payload).should == "hello you" |
|
54 |
+ end |
|
55 |
+ end |
|
56 |
+end |