@@ -167,6 +167,12 @@ EVENT_EXPIRATION_CHECK=6h |
||
167 | 167 |
# enabled. |
168 | 168 |
#USE_GRAPHVIZ_DOT=dot |
169 | 169 |
|
170 |
+# Default layout for agent flow diagrams generated by Graphviz. |
|
171 |
+# Choose from `circo`, `dot` (default), `fdp`, `neato`, `osage`, |
|
172 |
+# `patchwork`, `sfdp`, or `twopi`. Note that not all layouts are |
|
173 |
+# supported by Graphviz depending on the build options. |
|
174 |
+#DIAGRAM_DEFAULT_LAYOUT=dot |
|
175 |
+ |
|
170 | 176 |
# Timezone. Use `rake time:zones:local` or `rake time:zones:all` to get your zone name |
171 | 177 |
TIMEZONE="Pacific Time (US & Canada)" |
172 | 178 |
|
@@ -1,5 +1,17 @@ |
||
1 | 1 |
# Changes |
2 | 2 |
|
3 |
+* Oct 17, 2015 - TwitterSearchAgent added for running period Twitter searches. |
|
4 |
+* Oct 17, 2015 - GapDetectorAgent added to alert when no data has been seen in a certain period of time. |
|
5 |
+* Oct 12, 2015 - Slack agent supports attachments. |
|
6 |
+* Oct 9, 2015 - The TriggerAgent can be asked to match on fewer then all match groups. |
|
7 |
+* Oct 4, 2015 - Add DelayAgent for buffering incoming Events |
|
8 |
+* Oct 3, 2015 - Add SSL verification options to smtp.yml |
|
9 |
+* Oct 3, 2015 - Better handling of 'Back' links in the UI. |
|
10 |
+* Sep 22, 2015 - Comprehensive EvernoteAgent added |
|
11 |
+* Sep 13, 2015 - JavaScriptAgent can access and set Credentials. |
|
12 |
+* Sep 9, 2015 - Add AgentRunner and LongRunnable to support long running agents. |
|
13 |
+* Sep 8, 2015 - Allow `url_from_event` in the WebsiteAgent to be an Array |
|
14 |
+* Sep 7, 2015 - Enable `strict: false` in database.yml |
|
3 | 15 |
* Sep 2, 2015 - WebRequestConcern Agents automatically decode gzip/inflate encodings. |
4 | 16 |
* Sep 1, 2015 - WebhookAgent can configure allowed verbs (GET, POST, PUT, ...) for incoming requests. |
5 | 17 |
* Aug 21, 2015 - PostAgent supports "xml" as `content_type`. |
@@ -29,7 +29,7 @@ gem 'twitter-stream', github: 'cantino/twitter-stream', branch: 'huginn' |
||
29 | 29 |
gem 'omniauth-twitter' |
30 | 30 |
|
31 | 31 |
# Tumblr Agents |
32 |
-gem 'tumblr_client', github: 'knu/tumblr_client', branch: 'patch-1' |
|
32 |
+gem 'tumblr_client', github: 'tumblr/tumblr_client', branch: 'master' # '>= 0.8.5' |
|
33 | 33 |
gem 'omniauth-tumblr' |
34 | 34 |
|
35 | 35 |
# Dropbox Agents |
@@ -67,7 +67,7 @@ gem 'devise', '~> 3.4.0' |
||
67 | 67 |
gem 'dotenv-rails', '~> 2.0.1' |
68 | 68 |
gem 'em-http-request', '~> 1.1.2' |
69 | 69 |
gem 'faraday', '~> 0.9.0' |
70 |
-gem 'faraday_middleware', '>= 0.10.0' |
|
70 |
+gem 'faraday_middleware', github: 'lostisland/faraday_middleware', branch: 'master' # '>= 0.10.1' |
|
71 | 71 |
gem 'feed-normalizer' |
72 | 72 |
gem 'font-awesome-sass', '~> 4.3.2' |
73 | 73 |
gem 'foreman', '~> 0.63.0' |
@@ -20,9 +20,17 @@ GIT |
||
20 | 20 |
rest-client (~> 1.8) |
21 | 21 |
|
22 | 22 |
GIT |
23 |
- remote: git://github.com/knu/tumblr_client.git |
|
24 |
- revision: d6f1f64a7cba381345c588e28ebcff28048c3a6c |
|
25 |
- branch: patch-1 |
|
23 |
+ remote: git://github.com/lostisland/faraday_middleware.git |
|
24 |
+ revision: c5836ae55857272732b33eb0e0a98d60e995a376 |
|
25 |
+ branch: master |
|
26 |
+ specs: |
|
27 |
+ faraday_middleware (0.10.0) |
|
28 |
+ faraday (>= 0.7.4, < 0.10) |
|
29 |
+ |
|
30 |
+GIT |
|
31 |
+ remote: git://github.com/tumblr/tumblr_client.git |
|
32 |
+ revision: 0c59b04e49f2a8c89860613b18cf4e8f978d8dc7 |
|
33 |
+ branch: master |
|
26 | 34 |
specs: |
27 | 35 |
tumblr_client (0.8.5) |
28 | 36 |
faraday (~> 0.9.0) |
@@ -187,8 +195,6 @@ GEM |
||
187 | 195 |
extlib (0.9.16) |
188 | 196 |
faraday (0.9.1) |
189 | 197 |
multipart-post (>= 1.2, < 3) |
190 |
- faraday_middleware (0.10.0) |
|
191 |
- faraday (>= 0.7.4, < 0.10) |
|
192 | 198 |
feed-normalizer (1.5.2) |
193 | 199 |
hpricot (>= 0.6) |
194 | 200 |
simple-rss (>= 1.1) |
@@ -293,7 +299,7 @@ GEM |
||
293 | 299 |
mime-types (2.6.1) |
294 | 300 |
mini_magick (4.2.3) |
295 | 301 |
mini_portile (0.6.2) |
296 |
- minitest (5.8.0) |
|
302 |
+ minitest (5.8.1) |
|
297 | 303 |
mqtt (0.3.1) |
298 | 304 |
multi_json (1.11.2) |
299 | 305 |
multi_xml (0.5.5) |
@@ -442,8 +448,8 @@ GEM |
||
442 | 448 |
tilt (~> 1.1) |
443 | 449 |
select2-rails (3.5.9.3) |
444 | 450 |
thor (~> 0.14) |
445 |
- shoulda-matchers (2.8.0) |
|
446 |
- activesupport (>= 3.0.0) |
|
451 |
+ shoulda-matchers (3.0.0) |
|
452 |
+ activesupport (>= 4.0.0) |
|
447 | 453 |
signet (0.5.1) |
448 | 454 |
addressable (>= 2.2.3) |
449 | 455 |
faraday (>= 0.9.0.rc5) |
@@ -553,7 +559,7 @@ DEPENDENCIES |
||
553 | 559 |
em-http-request (~> 1.1.2) |
554 | 560 |
evernote_oauth |
555 | 561 |
faraday (~> 0.9.0) |
556 |
- faraday_middleware (>= 0.10.0) |
|
562 |
+ faraday_middleware! |
|
557 | 563 |
feed-normalizer |
558 | 564 |
ffi (>= 1.9.4) |
559 | 565 |
font-awesome-sass (~> 4.3.2) |
@@ -3,6 +3,7 @@ |
||
3 | 3 |
$ -> |
4 | 4 |
svg = document.querySelector('.agent-diagram svg.diagram') |
5 | 5 |
overlay = document.querySelector('.agent-diagram .overlay') |
6 |
+ $(overlay).width($(svg).width()).height($(svg).height()) |
|
6 | 7 |
getTopLeft = (node) -> |
7 | 8 |
bbox = node.getBBox() |
8 | 9 |
point = svg.createSVGPoint() |
@@ -7,7 +7,7 @@ module AgentControllerConcern |
||
7 | 7 |
|
8 | 8 |
def default_options |
9 | 9 |
{ |
10 |
- 'action' => 'run', |
|
10 |
+ 'action' => 'run' |
|
11 | 11 |
} |
12 | 12 |
end |
13 | 13 |
|
@@ -68,7 +68,7 @@ module AgentControllerConcern |
||
68 | 68 |
log "Agent '#{target.name}' is disabled" |
69 | 69 |
end |
70 | 70 |
when 'configure' |
71 |
- target.update!(options: target.options.merge(interpolated['configure_options'])) |
|
71 |
+ target.update! options: target.options.deep_merge(interpolated['configure_options']) |
|
72 | 72 |
log "Agent '#{target.name}' is configured with #{interpolated['configure_options'].inspect}" |
73 | 73 |
when '' |
74 | 74 |
# Do nothing |
@@ -51,12 +51,13 @@ module LongRunnable |
||
51 | 51 |
end |
52 | 52 |
|
53 | 53 |
class Worker |
54 |
- attr_reader :thread, :id, :agent, :config, :mutex, :scheduler |
|
54 |
+ attr_reader :thread, :id, :agent, :config, :mutex, :scheduler, :restarting |
|
55 | 55 |
|
56 | 56 |
def initialize(options = {}) |
57 | 57 |
@id = options[:id] |
58 | 58 |
@agent = options[:agent] |
59 | 59 |
@config = options[:config] |
60 |
+ @restarting = false |
|
60 | 61 |
end |
61 | 62 |
|
62 | 63 |
def run |
@@ -65,6 +66,7 @@ module LongRunnable |
||
65 | 66 |
|
66 | 67 |
def run! |
67 | 68 |
@thread = Thread.new do |
69 |
+ Thread.current[:name] = "#{id}-#{Time.now}" |
|
68 | 70 |
begin |
69 | 71 |
run |
70 | 72 |
rescue SignalException, SystemExit |
@@ -90,14 +92,21 @@ module LongRunnable |
||
90 | 92 |
if respond_to?(:stop) |
91 | 93 |
stop |
92 | 94 |
else |
93 |
- thread.terminate |
|
95 |
+ terminate_thread! |
|
94 | 96 |
end |
95 | 97 |
end |
96 | 98 |
|
99 |
+ def terminate_thread! |
|
100 |
+ thread.terminate |
|
101 |
+ thread.wakeup if thread.status == 'sleep' |
|
102 |
+ end |
|
103 |
+ |
|
97 | 104 |
def restart! |
98 |
- stop! |
|
99 |
- setup!(scheduler, mutex) |
|
100 |
- run! |
|
105 |
+ without_alive_check do |
|
106 |
+ stop! |
|
107 |
+ setup!(scheduler, mutex) |
|
108 |
+ run! |
|
109 |
+ end |
|
101 | 110 |
end |
102 | 111 |
|
103 | 112 |
def every(*args, &blk) |
@@ -120,5 +129,12 @@ module LongRunnable |
||
120 | 129 |
def schedule(method, args, &blk) |
121 | 130 |
@scheduler.send(method, *args, tag: id, &blk) |
122 | 131 |
end |
132 |
+ |
|
133 |
+ def without_alive_check(&blk) |
|
134 |
+ @restarting = true |
|
135 |
+ yield |
|
136 |
+ ensure |
|
137 |
+ @restarting = false |
|
138 |
+ end |
|
123 | 139 |
end |
124 | 140 |
end |
@@ -11,7 +11,7 @@ module SortableEvents |
||
11 | 11 |
|
12 | 12 |
module ClassMethods |
13 | 13 |
def can_order_created_events! |
14 |
- raise if cannot_create_events? |
|
14 |
+ raise 'Cannot order events for agent that cannot create events' if cannot_create_events? |
|
15 | 15 |
prepend AutomaticSorter |
16 | 16 |
end |
17 | 17 |
|
@@ -39,7 +39,7 @@ module WebRequestConcern |
||
39 | 39 |
# detection, so we do that. |
40 | 40 |
case env[:response_headers][:content_type] |
41 | 41 |
when /;\s*charset\s*=\s*([^()<>@,;:\\\"\/\[\]?={}\s]+)/i |
42 |
- encoding = Encoding.find($1) rescue nil |
|
42 |
+ encoding = Encoding.find($1) rescue @default_encoding |
|
43 | 43 |
when /\A\s*(?:text\/[^\s;]+|application\/(?:[^\s;]+\+)?(?:xml|json))\s*(?:;|\z)/i |
44 | 44 |
encoding = @default_encoding |
45 | 45 |
else |
@@ -47,7 +47,7 @@ module WebRequestConcern |
||
47 | 47 |
next |
48 | 48 |
end |
49 | 49 |
end |
50 |
- body.encode!(Encoding::UTF_8, encoding) unless body.encoding == Encoding::UTF_8 |
|
50 |
+ body.encode!(Encoding::UTF_8, encoding) |
|
51 | 51 |
end |
52 | 52 |
end |
53 | 53 |
end |
@@ -123,11 +123,6 @@ module WebRequestConcern |
||
123 | 123 |
|
124 | 124 |
builder.use FaradayMiddleware::Gzip |
125 | 125 |
|
126 |
- unless builder.headers.any? { |key,| /\Aaccept[-_]encoding\z/i =~ key } |
|
127 |
- # Exclude `deflate` by default. See #1018. |
|
128 |
- builder.headers[:accept_encoding] = 'gzip,identity' |
|
129 |
- end |
|
130 |
- |
|
131 | 126 |
case backend = faraday_backend |
132 | 127 |
when :typhoeus |
133 | 128 |
require 'typhoeus/adapters/faraday' |
@@ -1,8 +1,8 @@ |
||
1 | 1 |
module DotHelper |
2 |
- def render_agents_diagram(agents) |
|
2 |
+ def render_agents_diagram(agents, layout: nil) |
|
3 | 3 |
if (command = ENV['USE_GRAPHVIZ_DOT']) && |
4 | 4 |
(svg = IO.popen([command, *%w[-Tsvg -q1 -o/dev/stdout /dev/stdin]], 'w+') { |dot| |
5 |
- dot.print agents_dot(agents, true) |
|
5 |
+ dot.print agents_dot(agents, rich: true, layout: layout) |
|
6 | 6 |
dot.close_write |
7 | 7 |
dot.read |
8 | 8 |
} rescue false) |
@@ -125,7 +125,7 @@ module DotHelper |
||
125 | 125 |
DotDrawer.draw(vars, &block) |
126 | 126 |
end |
127 | 127 |
|
128 |
- def agents_dot(agents, rich = false) |
|
128 |
+ def agents_dot(agents, rich: false, layout: nil) |
|
129 | 129 |
draw(agents: agents, |
130 | 130 |
agent_id: ->agent { 'a%d' % agent.id }, |
131 | 131 |
agent_label: ->agent { |
@@ -158,7 +158,10 @@ module DotHelper |
||
158 | 158 |
end |
159 | 159 |
|
160 | 160 |
block('digraph', 'Agent Event Flow') { |
161 |
- # statement 'graph', rankdir: 'LR' |
|
161 |
+ layout ||= ENV['DIAGRAM_DEFAULT_LAYOUT'].presence |
|
162 |
+ if rich && /\A[a-z]+\z/ === layout |
|
163 |
+ statement 'graph', layout: layout, overlap: 'false' |
|
164 |
+ end |
|
162 | 165 |
statement 'node', |
163 | 166 |
shape: 'box', |
164 | 167 |
style: 'rounded', |
@@ -197,7 +200,6 @@ module DotHelper |
||
197 | 200 |
root << svg |
198 | 201 |
root << overlay_container = Nokogiri::XML::Node.new('div', doc) { |div| |
199 | 202 |
div['class'] = 'overlay-container' |
200 |
- div['style'] = "width: #{svg['width']}; height: #{svg['height']}" |
|
201 | 203 |
} |
202 | 204 |
overlay_container << overlay = Nokogiri::XML::Node.new('div', doc) { |div| |
203 | 205 |
div['class'] = 'overlay' |
@@ -0,0 +1,128 @@ |
||
1 |
+module Agents |
|
2 |
+ class BeeperAgent < Agent |
|
3 |
+ cannot_be_scheduled! |
|
4 |
+ cannot_create_events! |
|
5 |
+ |
|
6 |
+ description <<-MD |
|
7 |
+ Beeper agent sends messages to Beeper app on your mobile device via Push notifications. |
|
8 |
+ |
|
9 |
+ You need a Beeper Application ID (`app_id`), Beeper REST API Key (`api_key`) and Beeper Sender ID (`sender_id`) [https://beeper.io](https://beeper.io) |
|
10 |
+ |
|
11 |
+ You have to provide phone number (`phone`) of the recipient which have a mobile device with Beeper installed, or a `group_id` – Beeper Group ID |
|
12 |
+ |
|
13 |
+ Also you have to provide a message `type` which has to be `message`, `image`, `event`, `location` or `task`. |
|
14 |
+ |
|
15 |
+ Depending on message type you have to provide additional fields: |
|
16 |
+ |
|
17 |
+ ##### Message |
|
18 |
+ * `text` – **required** |
|
19 |
+ |
|
20 |
+ ##### Image |
|
21 |
+ * `image` – **required** (Image URL or Base64-encoded image) |
|
22 |
+ * `text` – optional |
|
23 |
+ |
|
24 |
+ ##### Event |
|
25 |
+ * `text` – **required** |
|
26 |
+ * `start_time` – **required** (Corresponding to ISO 8601) |
|
27 |
+ * `end_time` – optional (Corresponding to ISO 8601) |
|
28 |
+ |
|
29 |
+ ##### Location |
|
30 |
+ * `latitude` – **required** |
|
31 |
+ * `longitude` – **required** |
|
32 |
+ * `text` – optional |
|
33 |
+ |
|
34 |
+ ##### Task |
|
35 |
+ * `text` – **required** |
|
36 |
+ |
|
37 |
+ You can see additional documentation at [Beeper website](https://beeper.io/docs) |
|
38 |
+ MD |
|
39 |
+ |
|
40 |
+ BASE_URL = 'https://api.beeper.io/api' |
|
41 |
+ |
|
42 |
+ TYPE_ATTRIBUTES = { |
|
43 |
+ 'message' => %w(text), |
|
44 |
+ 'image' => %w(text image), |
|
45 |
+ 'event' => %w(text start_time end_time), |
|
46 |
+ 'location' => %w(text latitude longitude), |
|
47 |
+ 'task' => %w(text) |
|
48 |
+ } |
|
49 |
+ |
|
50 |
+ MESSAGE_TYPES = TYPE_ATTRIBUTES.keys |
|
51 |
+ |
|
52 |
+ TYPE_REQUIRED_ATTRIBUTES = { |
|
53 |
+ 'message' => %w(text), |
|
54 |
+ 'image' => %w(image), |
|
55 |
+ 'event' => %w(text start_time), |
|
56 |
+ 'location' => %w(latitude longitude), |
|
57 |
+ 'task' => %w(text) |
|
58 |
+ } |
|
59 |
+ |
|
60 |
+ def default_options |
|
61 |
+ { |
|
62 |
+ 'type' => 'message', |
|
63 |
+ 'app_id' => '', |
|
64 |
+ 'api_key' => '', |
|
65 |
+ 'sender_id' => '', |
|
66 |
+ 'phone' => '', |
|
67 |
+ 'text' => '{{title}}' |
|
68 |
+ } |
|
69 |
+ end |
|
70 |
+ |
|
71 |
+ def validate_options |
|
72 |
+ %w(app_id api_key sender_id type).each do |attr| |
|
73 |
+ errors.add(:base, "you need to specify a #{attr}") if options[attr].blank? |
|
74 |
+ end |
|
75 |
+ |
|
76 |
+ if options['type'].in?(MESSAGE_TYPES) |
|
77 |
+ required_attributes = TYPE_REQUIRED_ATTRIBUTES[options['type']] |
|
78 |
+ if required_attributes.any? { |attr| options[attr].blank? } |
|
79 |
+ errors.add(:base, "you need to specify a #{required_attributes.join(', ')}") |
|
80 |
+ end |
|
81 |
+ else |
|
82 |
+ errors.add(:base, 'you need to specify a valid message type') |
|
83 |
+ end |
|
84 |
+ |
|
85 |
+ unless options['group_id'].blank? ^ options['phone'].blank? |
|
86 |
+ errors.add(:base, 'you need to specify a phone or group_id') |
|
87 |
+ end |
|
88 |
+ end |
|
89 |
+ |
|
90 |
+ def working? |
|
91 |
+ received_event_without_error? && !recent_error_logs? |
|
92 |
+ end |
|
93 |
+ |
|
94 |
+ def receive(incoming_events) |
|
95 |
+ incoming_events.each do |event| |
|
96 |
+ send_message(event) |
|
97 |
+ end |
|
98 |
+ end |
|
99 |
+ |
|
100 |
+ def send_message(event) |
|
101 |
+ mo = interpolated(event) |
|
102 |
+ begin |
|
103 |
+ response = HTTParty.post(endpoint_for(mo['type']), body: payload_for(mo), headers: headers) |
|
104 |
+ error(response.body) if response.code != 201 |
|
105 |
+ rescue HTTParty::Error => e |
|
106 |
+ error(e.message) |
|
107 |
+ end |
|
108 |
+ end |
|
109 |
+ |
|
110 |
+ private |
|
111 |
+ |
|
112 |
+ def headers |
|
113 |
+ { |
|
114 |
+ 'X-Beeper-Application-Id' => options['app_id'], |
|
115 |
+ 'X-Beeper-REST-API-Key' => options['api_key'], |
|
116 |
+ 'Content-Type' => 'application/json' |
|
117 |
+ } |
|
118 |
+ end |
|
119 |
+ |
|
120 |
+ def payload_for(mo) |
|
121 |
+ mo.slice(*TYPE_ATTRIBUTES[mo['type']], 'sender_id', 'phone', 'group_id').to_json |
|
122 |
+ end |
|
123 |
+ |
|
124 |
+ def endpoint_for(type) |
|
125 |
+ "#{BASE_URL}/#{type}s.json" |
|
126 |
+ end |
|
127 |
+ end |
|
128 |
+end |
@@ -36,7 +36,7 @@ module Agents |
||
36 | 36 |
true |
37 | 37 |
end |
38 | 38 |
|
39 |
- def check! |
|
39 |
+ def check |
|
40 | 40 |
control! |
41 | 41 |
end |
42 | 42 |
|
@@ -1,5 +1,7 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class DataOutputAgent < Agent |
3 |
+ include WebRequestConcern |
|
4 |
+ |
|
3 | 5 |
cannot_be_scheduled! |
4 | 6 |
|
5 | 7 |
description do |
@@ -19,9 +21,10 @@ 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 values. Use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to format the values. Values of the `link`, `title`, `description` and `icon` keys will be put into the \\<channel\\> section of RSS output. The `item` key will be repeated for every Event. The `pubDate` key for each item will have the creation time of the Event unless given. |
|
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. Values of the `link`, `title`, `description` and `icon` keys will be put into the \\<channel\\> section of RSS output. Value of the `self` key will be used as URL for this feed itself, which is useful when you serve it via reverse proxy. The `item` key will be repeated for every Event. The `pubDate` key for each item will have the creation time of the Event unless given. |
|
23 | 25 |
* `events_to_show` - The number of events to output in RSS or JSON. (default: `40`) |
24 | 26 |
* `ttl` - A value for the \\<ttl\\> element in RSS output. (default: `60`) |
27 |
+ * `push_hubs` - Set to a list of PubSubHubbub endpoints you want to publish an update to every time this agent receives an event. (default: none) Popular hubs include [Superfeedr](https://pubsubhubbub.superfeedr.com/) and [Google](https://pubsubhubbub.appspot.com/). Note that publishing updates will make your feed URL known to the public, so if you want to keep it secret, set up a reverse proxy to serve your feed via a safe URL and specify it in `template.self`. |
|
25 | 28 |
|
26 | 29 |
If you'd like to output RSS tags with attributes, such as `enclosure`, use something like the following in your `template`: |
27 | 30 |
|
@@ -95,6 +98,29 @@ module Agents |
||
95 | 98 |
unless options['template'].present? && options['template']['item'].present? && options['template']['item'].is_a?(Hash) |
96 | 99 |
errors.add(:base, "Please provide template and template.item") |
97 | 100 |
end |
101 |
+ |
|
102 |
+ case options['push_hubs'] |
|
103 |
+ when nil |
|
104 |
+ when Array |
|
105 |
+ options['push_hubs'].each do |hub| |
|
106 |
+ case hub |
|
107 |
+ when /\{/ |
|
108 |
+ # Liquid templating |
|
109 |
+ when String |
|
110 |
+ begin |
|
111 |
+ URI.parse(hub) |
|
112 |
+ rescue URI::Error |
|
113 |
+ errors.add(:base, "invalid URL found in push_hubs") |
|
114 |
+ break |
|
115 |
+ end |
|
116 |
+ else |
|
117 |
+ errors.add(:base, "push_hubs must be an array of endpoint URLs") |
|
118 |
+ break |
|
119 |
+ end |
|
120 |
+ end |
|
121 |
+ else |
|
122 |
+ errors.add(:base, "push_hubs must be an array") |
|
123 |
+ end |
|
98 | 124 |
end |
99 | 125 |
|
100 | 126 |
def events_to_show |
@@ -114,11 +140,12 @@ module Agents |
||
114 | 140 |
end |
115 | 141 |
|
116 | 142 |
def feed_url(options = {}) |
117 |
- feed_link + Rails.application.routes.url_helpers. |
|
118 |
- web_requests_path(agent_id: id || ':id', |
|
119 |
- user_id: user_id, |
|
120 |
- secret: options[:secret], |
|
121 |
- format: options[:format]) |
|
143 |
+ interpolated['template']['self'].presence || |
|
144 |
+ feed_link + Rails.application.routes.url_helpers. |
|
145 |
+ web_requests_path(agent_id: id || ':id', |
|
146 |
+ user_id: user_id, |
|
147 |
+ secret: options[:secret], |
|
148 |
+ format: options[:format]) |
|
122 | 149 |
end |
123 | 150 |
|
124 | 151 |
def feed_icon |
@@ -129,6 +156,10 @@ module Agents |
||
129 | 156 |
interpolated['template']['description'].presence || "A feed of Events received by the '#{name}' Huginn Agent" |
130 | 157 |
end |
131 | 158 |
|
159 |
+ def push_hubs |
|
160 |
+ interpolated['push_hubs'].presence || [] |
|
161 |
+ end |
|
162 |
+ |
|
132 | 163 |
def receive_web_request(params, method, format) |
133 | 164 |
unless interpolated['secrets'].include?(params['secret']) |
134 | 165 |
if format =~ /json/ |
@@ -159,40 +190,54 @@ module Agents |
||
159 | 190 |
interpolated |
160 | 191 |
end |
161 | 192 |
|
193 |
+ now = Time.now |
|
194 |
+ |
|
162 | 195 |
if format =~ /json/ |
163 | 196 |
content = { |
164 | 197 |
'title' => feed_title, |
165 | 198 |
'description' => feed_description, |
166 |
- 'pubDate' => Time.now, |
|
199 |
+ 'pubDate' => now, |
|
167 | 200 |
'items' => simplify_item_for_json(items) |
168 | 201 |
} |
169 | 202 |
|
170 | 203 |
return [content, 200] |
171 | 204 |
else |
172 |
- content = Utils.unindent(<<-XML) |
|
173 |
- <?xml version="1.0" encoding="UTF-8" ?> |
|
174 |
- <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> |
|
175 |
- <channel> |
|
176 |
- <atom:link href=#{feed_url(secret: params['secret'], format: :xml).encode(xml: :attr)} rel="self" type="application/rss+xml" /> |
|
177 |
- <atom:icon>#{feed_icon.encode(xml: :text)}</atom:icon> |
|
178 |
- <title>#{feed_title.encode(xml: :text)}</title> |
|
179 |
- <description>#{feed_description.encode(xml: :text)}</description> |
|
180 |
- <link>#{feed_link.encode(xml: :text)}</link> |
|
181 |
- <lastBuildDate>#{Time.now.rfc2822.to_s.encode(xml: :text)}</lastBuildDate> |
|
182 |
- <pubDate>#{Time.now.rfc2822.to_s.encode(xml: :text)}</pubDate> |
|
183 |
- <ttl>#{feed_ttl}</ttl> |
|
184 |
- |
|
205 |
+ hub_links = push_hubs.map { |hub| |
|
206 |
+ <<-XML |
|
207 |
+ <atom:link rel="hub" href=#{hub.encode(xml: :attr)}/> |
|
208 |
+ XML |
|
209 |
+ }.join |
|
210 |
+ |
|
211 |
+ items = simplify_item_for_xml(items) |
|
212 |
+ .to_xml(skip_types: true, root: "items", skip_instruct: true, indent: 1) |
|
213 |
+ .gsub(%r{^</?items>\n}, '') |
|
214 |
+ |
|
215 |
+ return [<<-XML, 200, 'text/xml'] |
|
216 |
+<?xml version="1.0" encoding="UTF-8" ?> |
|
217 |
+<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"> |
|
218 |
+<channel> |
|
219 |
+ <atom:link href=#{feed_url(secret: params['secret'], format: :xml).encode(xml: :attr)} rel="self" type="application/rss+xml" /> |
|
220 |
+ <atom:icon>#{feed_icon.encode(xml: :text)}</atom:icon> |
|
221 |
+#{hub_links} |
|
222 |
+ <title>#{feed_title.encode(xml: :text)}</title> |
|
223 |
+ <description>#{feed_description.encode(xml: :text)}</description> |
|
224 |
+ <link>#{feed_link.encode(xml: :text)}</link> |
|
225 |
+ <lastBuildDate>#{now.rfc2822.to_s.encode(xml: :text)}</lastBuildDate> |
|
226 |
+ <pubDate>#{now.rfc2822.to_s.encode(xml: :text)}</pubDate> |
|
227 |
+ <ttl>#{feed_ttl}</ttl> |
|
228 |
+#{items} |
|
229 |
+</channel> |
|
230 |
+</rss> |
|
185 | 231 |
XML |
232 |
+ end |
|
233 |
+ end |
|
234 |
+ end |
|
186 | 235 |
|
187 |
- content += simplify_item_for_xml(items).to_xml(skip_types: true, root: "items", skip_instruct: true, indent: 1).gsub(/^<\/?items>/, '').strip |
|
188 |
- |
|
189 |
- content += Utils.unindent(<<-XML) |
|
190 |
- </channel> |
|
191 |
- </rss> |
|
192 |
- XML |
|
236 |
+ def receive(incoming_events) |
|
237 |
+ url = feed_url(secret: interpolated['secrets'].first, format: :xml) |
|
193 | 238 |
|
194 |
- return [content, 200, 'text/xml'] |
|
195 |
- end |
|
239 |
+ push_hubs.each do |hub| |
|
240 |
+ push_to_hub(hub, url) |
|
196 | 241 |
end |
197 | 242 |
end |
198 | 243 |
|
@@ -261,5 +306,32 @@ module Agents |
||
261 | 306 |
item |
262 | 307 |
end |
263 | 308 |
end |
309 |
+ |
|
310 |
+ def push_to_hub(hub, url) |
|
311 |
+ hub_uri = |
|
312 |
+ begin |
|
313 |
+ URI.parse(hub) |
|
314 |
+ rescue URI::Error |
|
315 |
+ nil |
|
316 |
+ end |
|
317 |
+ |
|
318 |
+ if !hub_uri.is_a?(URI::HTTP) |
|
319 |
+ error "Invalid push endpoint: #{hub}" |
|
320 |
+ return |
|
321 |
+ end |
|
322 |
+ |
|
323 |
+ log "Pushing #{url} to #{hub_uri}" |
|
324 |
+ |
|
325 |
+ return if dry_run? |
|
326 |
+ |
|
327 |
+ begin |
|
328 |
+ faraday.post hub_uri, { |
|
329 |
+ 'hub.mode' => 'publish', |
|
330 |
+ 'hub.url' => url |
|
331 |
+ } |
|
332 |
+ rescue => e |
|
333 |
+ error "Push failed: #{e.message}" |
|
334 |
+ end |
|
335 |
+ end |
|
264 | 336 |
end |
265 | 337 |
end |
@@ -0,0 +1,73 @@ |
||
1 |
+module Agents |
|
2 |
+ class DelayAgent < Agent |
|
3 |
+ default_schedule "every_12h" |
|
4 |
+ |
|
5 |
+ description <<-MD |
|
6 |
+ The DelayAgent stores received Events and emits copies of them on a schedule. Use this as a buffer or queue of Events. |
|
7 |
+ |
|
8 |
+ `max_events` should be set to the maximum number of events that you'd like to hold in the buffer. When this number is |
|
9 |
+ reached, new events will either be ignored, or will displace the oldest event already in the buffer, depending on |
|
10 |
+ whether you set `keep` to `newest` or `oldest`. |
|
11 |
+ |
|
12 |
+ `expected_receive_period_in_days` is used to determine if the Agent is working. Set it to the maximum number of days |
|
13 |
+ that you anticipate passing without this Agent receiving an incoming Event. |
|
14 |
+ |
|
15 |
+ `max_emitted_events` is used to limit the number of the maximum events which should be created. If you omit this DelayAgent will create events for every event stored in the memory. |
|
16 |
+ MD |
|
17 |
+ |
|
18 |
+ def default_options |
|
19 |
+ { |
|
20 |
+ 'expected_receive_period_in_days' => "10", |
|
21 |
+ 'max_events' => "100", |
|
22 |
+ 'keep' => 'newest' |
|
23 |
+ } |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ def validate_options |
|
27 |
+ unless options['expected_receive_period_in_days'].present? && options['expected_receive_period_in_days'].to_i > 0 |
|
28 |
+ errors.add(:base, "Please provide 'expected_receive_period_in_days' to indicate how many days can pass before this Agent is considered to be not working") |
|
29 |
+ end |
|
30 |
+ |
|
31 |
+ unless options['keep'].present? && options['keep'].in?(%w[newest oldest]) |
|
32 |
+ errors.add(:base, "The 'keep' option is required and must be set to 'oldest' or 'newest'") |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ unless options['max_events'].present? && options['max_events'].to_i > 0 |
|
36 |
+ errors.add(:base, "The 'max_events' option is required and must be an integer greater than 0") |
|
37 |
+ end |
|
38 |
+ end |
|
39 |
+ |
|
40 |
+ def working? |
|
41 |
+ last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? |
|
42 |
+ end |
|
43 |
+ |
|
44 |
+ def receive(incoming_events) |
|
45 |
+ incoming_events.each do |event| |
|
46 |
+ memory['event_ids'] ||= [] |
|
47 |
+ memory['event_ids'] << event.id |
|
48 |
+ if memory['event_ids'].length > interpolated['max_events'].to_i |
|
49 |
+ if interpolated['keep'] == 'newest' |
|
50 |
+ memory['event_ids'].shift |
|
51 |
+ else |
|
52 |
+ memory['event_ids'].pop |
|
53 |
+ end |
|
54 |
+ end |
|
55 |
+ end |
|
56 |
+ end |
|
57 |
+ |
|
58 |
+ def check |
|
59 |
+ if memory['event_ids'] && memory['event_ids'].length > 0 |
|
60 |
+ events = received_events.where(id: memory['event_ids']).reorder('events.id asc') |
|
61 |
+ |
|
62 |
+ if options['max_emitted_events'].present? |
|
63 |
+ events = events.limit(options['max_emitted_events'].to_i) |
|
64 |
+ end |
|
65 |
+ |
|
66 |
+ events.each do |event| |
|
67 |
+ create_event payload: event.payload |
|
68 |
+ memory['event_ids'].delete(event.id) |
|
69 |
+ end |
|
70 |
+ end |
|
71 |
+ end |
|
72 |
+ end |
|
73 |
+end |
@@ -1,6 +1,7 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class EventFormattingAgent < Agent |
3 | 3 |
cannot_be_scheduled! |
4 |
+ can_dry_run! |
|
4 | 5 |
|
5 | 6 |
description <<-MD |
6 | 7 |
The Event Formatting Agent allows you to format incoming Events, adding new fields as needed. |
@@ -196,7 +196,7 @@ module Agents |
||
196 | 196 |
end |
197 | 197 |
|
198 | 198 |
def uri_path_escape(string) |
199 |
- str = string.dup.force_encoding(Encoding::ASCII_8BIT) # string.b in Ruby >=2.0 |
|
199 |
+ str = string.b |
|
200 | 200 |
str.gsub!(/([^A-Za-z0-9\-._~!$&()*+,=@]+)/) { |m| |
201 | 201 |
'%' + m.unpack('H2' * m.bytesize).join('%').upcase |
202 | 202 |
} |
@@ -0,0 +1,67 @@ |
||
1 |
+module Agents |
|
2 |
+ class GapDetectorAgent < Agent |
|
3 |
+ default_schedule "every_10m" |
|
4 |
+ |
|
5 |
+ description <<-MD |
|
6 |
+ The Gap Detector Agent will watch for holes or gaps in a stream of incoming Events and generate "no data alerts". |
|
7 |
+ |
|
8 |
+ The `value_path` value is a [JSONPath](http://goessner.net/articles/JsonPath/) to a value of interest. If either |
|
9 |
+ this value is empty, or no Events are received, during `window_duration_in_days`, an Event will be created with |
|
10 |
+ a payload of `message`. |
|
11 |
+ MD |
|
12 |
+ |
|
13 |
+ event_description <<-MD |
|
14 |
+ Events look like: |
|
15 |
+ |
|
16 |
+ { |
|
17 |
+ "message": "No data has been received!", |
|
18 |
+ "gap_started_at": "1234567890" |
|
19 |
+ } |
|
20 |
+ MD |
|
21 |
+ |
|
22 |
+ def validate_options |
|
23 |
+ unless options['message'].present? |
|
24 |
+ errors.add(:base, "message is required") |
|
25 |
+ end |
|
26 |
+ |
|
27 |
+ unless options['window_duration_in_days'].present? && options['window_duration_in_days'].to_f > 0 |
|
28 |
+ errors.add(:base, "window_duration_in_days must be provided as an integer or floating point number") |
|
29 |
+ end |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ def default_options |
|
33 |
+ { |
|
34 |
+ 'window_duration_in_days' => "2", |
|
35 |
+ 'message' => "No data has been received!" |
|
36 |
+ } |
|
37 |
+ end |
|
38 |
+ |
|
39 |
+ def working? |
|
40 |
+ true |
|
41 |
+ end |
|
42 |
+ |
|
43 |
+ def receive(incoming_events) |
|
44 |
+ incoming_events.sort_by(&:created_at).each do |event| |
|
45 |
+ memory['newest_event_created_at'] ||= 0 |
|
46 |
+ |
|
47 |
+ if !interpolated['value_path'].present? || Utils.value_at(event.payload, interpolated['value_path']).present? |
|
48 |
+ if event.created_at.to_i > memory['newest_event_created_at'] |
|
49 |
+ memory['newest_event_created_at'] = event.created_at.to_i |
|
50 |
+ memory.delete('alerted_at') |
|
51 |
+ end |
|
52 |
+ end |
|
53 |
+ end |
|
54 |
+ end |
|
55 |
+ |
|
56 |
+ def check |
|
57 |
+ window = interpolated['window_duration_in_days'].to_f.days.ago |
|
58 |
+ if memory['newest_event_created_at'].present? && Time.at(memory['newest_event_created_at']) < window |
|
59 |
+ unless memory['alerted_at'] |
|
60 |
+ memory['alerted_at'] = Time.now.to_i |
|
61 |
+ create_event payload: { message: interpolated['message'], |
|
62 |
+ gap_started_at: memory['newest_event_created_at'] } |
|
63 |
+ end |
|
64 |
+ end |
|
65 |
+ end |
|
66 |
+ end |
|
67 |
+end |
@@ -7,7 +7,7 @@ module Agents |
||
7 | 7 |
description <<-MD |
8 | 8 |
The Peak Detector Agent will 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 | 9 |
|
10 |
- 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. |
|
10 |
+ The `value_path` value is a [JSONPath](http://goessner.net/articles/JsonPath/) to the value of interest. `group_by_path` is a JSONPath that will be used to group values, if present. |
|
11 | 11 |
|
12 | 12 |
Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent. |
13 | 13 |
|
@@ -87,34 +87,41 @@ module Agents |
||
87 | 87 |
end |
88 | 88 |
|
89 | 89 |
def check |
90 |
- Array(interpolated['url']).each do |url| |
|
91 |
- check_url(url) |
|
92 |
- end |
|
90 |
+ check_urls(Array(interpolated['url'])) |
|
93 | 91 |
end |
94 | 92 |
|
95 | 93 |
protected |
96 | 94 |
|
97 |
- def check_url(url) |
|
98 |
- response = faraday.get(url) |
|
99 |
- if response.success? |
|
100 |
- feed = FeedNormalizer::FeedNormalizer.parse(response.body, loose: true) |
|
101 |
- feed.clean! if boolify(interpolated['clean']) |
|
102 |
- max_events = (interpolated['max_events_per_run'].presence || 0).to_i |
|
103 |
- created_event_count = 0 |
|
104 |
- sort_events(feed_to_events(feed)).each.with_index do |event, index| |
|
105 |
- break if max_events && max_events > 0 && index >= max_events |
|
106 |
- entry_id = event.payload[:id] |
|
107 |
- if check_and_track(entry_id) |
|
95 |
+ def check_urls(urls) |
|
96 |
+ new_events = [] |
|
97 |
+ max_events = (interpolated['max_events_per_run'].presence || 0).to_i |
|
98 |
+ |
|
99 |
+ urls.each do |url| |
|
100 |
+ begin |
|
101 |
+ response = faraday.get(url) |
|
102 |
+ if response.success? |
|
103 |
+ feed = FeedNormalizer::FeedNormalizer.parse(response.body, loose: true) |
|
104 |
+ feed.clean! if boolify(interpolated['clean']) |
|
105 |
+ new_events.concat feed_to_events(feed) |
|
106 |
+ else |
|
107 |
+ error "Failed to fetch #{url}: #{response.inspect}" |
|
108 |
+ end |
|
109 |
+ rescue => e |
|
110 |
+ error "Failed to fetch #{url} with message '#{e.message}': #{e.backtrace}" |
|
111 |
+ end |
|
112 |
+ end |
|
113 |
+ |
|
114 |
+ created_event_count = 0 |
|
115 |
+ sort_events(new_events).each.with_index do |event, index| |
|
116 |
+ entry_id = event.payload[:id] |
|
117 |
+ if check_and_track(entry_id) |
|
118 |
+ unless max_events && max_events > 0 && index >= max_events |
|
108 | 119 |
created_event_count += 1 |
109 | 120 |
create_event(event) |
110 | 121 |
end |
111 | 122 |
end |
112 |
- log "Fetched #{url} and created #{created_event_count} event(s)." |
|
113 |
- else |
|
114 |
- error "Failed to fetch #{url}: #{response.inspect}" |
|
115 | 123 |
end |
116 |
- rescue => e |
|
117 |
- error "Failed to fetch #{url} with message '#{e.message}': #{e.backtrace}" |
|
124 |
+ log "Fetched #{urls.to_sentence} and created #{created_event_count} event(s)." |
|
118 | 125 |
end |
119 | 126 |
|
120 | 127 |
def get_entry_id(entry) |
@@ -87,10 +87,6 @@ module Agents |
||
87 | 87 |
true |
88 | 88 |
end |
89 | 89 |
|
90 |
- def check! |
|
91 |
- control! |
|
92 |
- end |
|
93 |
- |
|
94 | 90 |
def validate_options |
95 | 91 |
if (spec = options['schedule']).present? |
96 | 92 |
begin |
@@ -1,9 +1,9 @@ |
||
1 |
-require 'open3' |
|
2 |
- |
|
3 | 1 |
module Agents |
4 | 2 |
class ShellCommandAgent < Agent |
5 | 3 |
default_schedule "never" |
6 | 4 |
|
5 |
+ can_dry_run! |
|
6 |
+ |
|
7 | 7 |
def self.should_run? |
8 | 8 |
ENV['ENABLE_INSECURE_AGENTS'] == "true" |
9 | 9 |
end |
@@ -11,7 +11,7 @@ module Agents |
||
11 | 11 |
description <<-MD |
12 | 12 |
The Shell Command Agent will execute commands on your local system, returning the output. |
13 | 13 |
|
14 |
- `command` specifies the command to be executed, and `path` will tell ShellCommandAgent in what directory to run this command. |
|
14 |
+ `command` specifies the command (either a shell command line string or an array of command line arguments) to be executed, and `path` will tell ShellCommandAgent in what directory to run this command. The content of `stdin` will be fed to the command via the standard input. |
|
15 | 15 |
|
16 | 16 |
`expected_update_period_in_days` is used to determine if the Agent is working. |
17 | 17 |
|
@@ -20,6 +20,10 @@ module Agents |
||
20 | 20 |
|
21 | 21 |
The resulting event will contain the `command` which was executed, the `path` it was executed under, the `exit_status` of the command, the `errors`, and the actual `output`. ShellCommandAgent will not log an error if the result implies that something went wrong. |
22 | 22 |
|
23 |
+ If `suppress_on_failure` is set to true, no event is emitted when `exit_status` is not zero. |
|
24 |
+ |
|
25 |
+ If `suppress_on_empty_output` is set to true, no event is emitted when `output` is empty. |
|
26 |
+ |
|
23 | 27 |
*Warning*: This type of Agent runs arbitrary commands on your system, #{Agents::ShellCommandAgent.should_run? ? "but is **currently enabled**" : "and is **currently disabled**"}. |
24 | 28 |
Only enable this Agent if you trust everyone using your Huginn installation. |
25 | 29 |
You can enable this Agent in your .env file by setting `ENABLE_INSECURE_AGENTS` to `true`. |
@@ -31,7 +35,7 @@ module Agents |
||
31 | 35 |
{ |
32 | 36 |
"command": "pwd", |
33 | 37 |
"path": "/home/Huginn", |
34 |
- "exit_status": "0", |
|
38 |
+ "exit_status": 0, |
|
35 | 39 |
"errors": "", |
36 | 40 |
"output": "/home/Huginn" |
37 | 41 |
} |
@@ -41,6 +45,8 @@ module Agents |
||
41 | 45 |
{ |
42 | 46 |
'path' => "/", |
43 | 47 |
'command' => "pwd", |
48 |
+ 'suppress_on_failure' => false, |
|
49 |
+ 'suppress_on_empty_output' => false, |
|
44 | 50 |
'expected_update_period_in_days' => 1 |
45 | 51 |
} |
46 | 52 |
end |
@@ -50,6 +56,16 @@ module Agents |
||
50 | 56 |
errors.add(:base, "The path, command, and expected_update_period_in_days fields are all required.") |
51 | 57 |
end |
52 | 58 |
|
59 |
+ case options['stdin'] |
|
60 |
+ when String, nil |
|
61 |
+ else |
|
62 |
+ errors.add(:base, "stdin must be a string.") |
|
63 |
+ end |
|
64 |
+ |
|
65 |
+ unless Array(options['command']).all? { |o| o.is_a?(String) } |
|
66 |
+ errors.add(:base, "command must be a shell command line string or an array of command line arguments.") |
|
67 |
+ end |
|
68 |
+ |
|
53 | 69 |
unless File.directory?(options['path']) |
54 | 70 |
errors.add(:base, "#{options['path']} is not a real directory.") |
55 | 71 |
end |
@@ -75,38 +91,62 @@ module Agents |
||
75 | 91 |
if Agents::ShellCommandAgent.should_run? |
76 | 92 |
command = opts['command'] |
77 | 93 |
path = opts['path'] |
94 |
+ stdin = opts['stdin'] |
|
95 |
+ |
|
96 |
+ result, errors, exit_status = run_command(path, command, stdin) |
|
78 | 97 |
|
79 |
- result, errors, exit_status = run_command(path, command) |
|
98 |
+ payload = { |
|
99 |
+ 'command' => command, |
|
100 |
+ 'path' => path, |
|
101 |
+ 'exit_status' => exit_status, |
|
102 |
+ 'errors' => errors, |
|
103 |
+ 'output' => result, |
|
104 |
+ } |
|
80 | 105 |
|
81 |
- vals = {"command" => command, "path" => path, "exit_status" => exit_status, "errors" => errors, "output" => result} |
|
82 |
- created_event = create_event :payload => vals |
|
106 |
+ unless suppress_event?(payload) |
|
107 |
+ created_event = create_event payload: payload |
|
108 |
+ end |
|
83 | 109 |
|
84 |
- log("Ran '#{command}' under '#{path}'", :outbound_event => created_event, :inbound_event => event) |
|
110 |
+ log("Ran '#{command}' under '#{path}'", outbound_event: created_event, inbound_event: event) |
|
85 | 111 |
else |
86 | 112 |
log("Unable to run because insecure agents are not enabled. Edit ENABLE_INSECURE_AGENTS in the Huginn .env configuration.") |
87 | 113 |
end |
88 | 114 |
end |
89 | 115 |
|
90 |
- def run_command(path, command) |
|
91 |
- result = nil |
|
92 |
- errors = nil |
|
93 |
- exit_status = nil |
|
94 |
- |
|
95 |
- Dir.chdir(path){ |
|
96 |
- begin |
|
97 |
- stdin, stdout, stderr, wait_thr = Open3.popen3(command) |
|
98 |
- exit_status = wait_thr.value.to_i |
|
99 |
- result = stdout.gets(nil) |
|
100 |
- errors = stderr.gets(nil) |
|
101 |
- rescue Exception => e |
|
102 |
- errors = e.to_s |
|
116 |
+ def run_command(path, command, stdin) |
|
117 |
+ begin |
|
118 |
+ rout, wout = IO.pipe |
|
119 |
+ rerr, werr = IO.pipe |
|
120 |
+ rin, win = IO.pipe |
|
121 |
+ |
|
122 |
+ pid = spawn(*command, chdir: path, out: wout, err: werr, in: rin) |
|
123 |
+ |
|
124 |
+ wout.close |
|
125 |
+ werr.close |
|
126 |
+ rin.close |
|
127 |
+ |
|
128 |
+ if stdin |
|
129 |
+ win.write stdin |
|
130 |
+ win.close |
|
103 | 131 |
end |
104 |
- } |
|
105 | 132 |
|
106 |
- result = result.to_s.strip |
|
107 |
- errors = errors.to_s.strip |
|
133 |
+ (result = rout.read).strip! |
|
134 |
+ (errors = rerr.read).strip! |
|
135 |
+ |
|
136 |
+ _, status = Process.wait2(pid) |
|
137 |
+ exit_status = status.exitstatus |
|
138 |
+ rescue => e |
|
139 |
+ errors = e.to_s |
|
140 |
+ result = ''.freeze |
|
141 |
+ exit_status = nil |
|
142 |
+ end |
|
108 | 143 |
|
109 | 144 |
[result, errors, exit_status] |
110 | 145 |
end |
146 |
+ |
|
147 |
+ def suppress_event?(payload) |
|
148 |
+ (boolify(interpolated['suppress_on_failure']) && payload['exit_status'].nonzero?) || |
|
149 |
+ (boolify(interpolated['suppress_on_empty_output']) && payload['output'].empty?) |
|
150 |
+ end |
|
111 | 151 |
end |
112 | 152 |
end |
@@ -1,6 +1,7 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class SlackAgent < Agent |
3 | 3 |
DEFAULT_USERNAME = 'Huginn' |
4 |
+ ALLOWED_PARAMS = ['channel', 'username', 'unfurl_links', 'attachments'] |
|
4 | 5 |
|
5 | 6 |
cannot_be_scheduled! |
6 | 7 |
cannot_create_events! |
@@ -13,7 +14,7 @@ module Agents |
||
13 | 14 |
#{'## Include `slack-notifier` in your Gemfile to use this Agent!' if dependencies_missing?} |
14 | 15 |
|
15 | 16 |
To get started, you will first need to configure an incoming webhook. |
16 |
- |
|
17 |
+ |
|
17 | 18 |
- Go to `https://my.slack.com/services/new/incoming-webhook`, choose a default channel and add the integration. |
18 | 19 |
|
19 | 20 |
Your webhook URL will look like: `https://hooks.slack.com/services/some/random/characters` |
@@ -65,14 +66,22 @@ module Agents |
||
65 | 66 |
@slack_notifier ||= Slack::Notifier.new(webhook_url, username: username) |
66 | 67 |
end |
67 | 68 |
|
69 |
+ def filter_options(opts) |
|
70 |
+ opts.select { |key, value| ALLOWED_PARAMS.include? key }.symbolize_keys |
|
71 |
+ end |
|
72 |
+ |
|
68 | 73 |
def receive(incoming_events) |
69 | 74 |
incoming_events.each do |event| |
70 | 75 |
opts = interpolated(event) |
71 |
- if /^:/.match(opts[:icon]) |
|
72 |
- slack_notifier.ping opts[:message], channel: opts[:channel], username: opts[:username], icon_emoji: opts[:icon], unfurl_links: opts[:unfurl_links] |
|
73 |
- else |
|
74 |
- slack_notifier.ping opts[:message], channel: opts[:channel], username: opts[:username], icon_url: opts[:icon], unfurl_links: opts[:unfurl_links] |
|
76 |
+ slack_opts = filter_options(opts) |
|
77 |
+ if opts[:icon].present? |
|
78 |
+ if /^:/.match(opts[:icon]) |
|
79 |
+ slack_opts[:icon_emoji] = opts[:icon] |
|
80 |
+ else |
|
81 |
+ slack_opts[:icon_url] = opts[:icon] |
|
82 |
+ end |
|
75 | 83 |
end |
84 |
+ slack_notifier.ping opts[:message], slack_opts |
|
76 | 85 |
end |
77 | 86 |
end |
78 | 87 |
end |
@@ -13,7 +13,10 @@ module Agents |
||
13 | 13 |
|
14 | 14 |
The `value` can be a single value or an array of values. In the case of an array, if one or more values match then the rule matches. |
15 | 15 |
|
16 |
- 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. |
|
16 |
+ By default, all rules must match for the Agent to trigger. You can switch this so that only one rule must match by |
|
17 |
+ setting `must_match` to `1`. |
|
18 |
+ |
|
19 |
+ 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 | 20 |
|
18 | 21 |
Set `keep_event` to `true` if you'd like to re-emit the incoming event, optionally merged with 'message' when provided. |
19 | 22 |
|
@@ -35,6 +38,14 @@ module Agents |
||
35 | 38 |
errors.add(:base, "message is required unless 'keep_event' is 'true'") unless options['message'].present? || keep_event? |
36 | 39 |
|
37 | 40 |
errors.add(:base, "keep_event, when present, must be 'true' or 'false'") unless options['keep_event'].blank? || %w[true false].include?(options['keep_event']) |
41 |
+ |
|
42 |
+ if options['must_match'].present? |
|
43 |
+ if options['must_match'].to_i < 1 |
|
44 |
+ errors.add(:base, "If used, the 'must_match' option must be a positive integer") |
|
45 |
+ elsif options['must_match'].to_i > options['rules'].length |
|
46 |
+ errors.add(:base, "If used, the 'must_match' option must be equal to or less than the number of rules") |
|
47 |
+ end |
|
48 |
+ end |
|
38 | 49 |
end |
39 | 50 |
|
40 | 51 |
def default_options |
@@ -59,12 +70,12 @@ module Agents |
||
59 | 70 |
|
60 | 71 |
opts = interpolated(event) |
61 | 72 |
|
62 |
- match = opts['rules'].all? do |rule| |
|
73 |
+ match_results = opts['rules'].map do |rule| |
|
63 | 74 |
value_at_path = Utils.value_at(event['payload'], rule['path']) |
64 | 75 |
rule_values = rule['value'] |
65 | 76 |
rule_values = [rule_values] unless rule_values.is_a?(Array) |
66 | 77 |
|
67 |
- match_found = rule_values.any? do |rule_value| |
|
78 |
+ rule_values.any? do |rule_value| |
|
68 | 79 |
case rule['type'] |
69 | 80 |
when "regex" |
70 | 81 |
value_at_path.to_s =~ Regexp.new(rule_value, Regexp::IGNORECASE) |
@@ -88,7 +99,7 @@ module Agents |
||
88 | 99 |
end |
89 | 100 |
end |
90 | 101 |
|
91 |
- if match |
|
102 |
+ if matches?(match_results) |
|
92 | 103 |
if keep_event? |
93 | 104 |
payload = event.payload.dup |
94 | 105 |
payload['message'] = opts['message'] if opts['message'].present? |
@@ -101,6 +112,14 @@ module Agents |
||
101 | 112 |
end |
102 | 113 |
end |
103 | 114 |
|
115 |
+ def matches?(matches) |
|
116 |
+ if options['must_match'].present? |
|
117 |
+ matches.select { |match| match }.length >= options['must_match'].to_i |
|
118 |
+ else |
|
119 |
+ matches.all? |
|
120 |
+ end |
|
121 |
+ end |
|
122 |
+ |
|
104 | 123 |
def keep_event? |
105 | 124 |
boolify(interpolated['keep_event']) |
106 | 125 |
end |
@@ -17,9 +17,9 @@ module Agents |
||
17 | 17 |
|
18 | 18 |
**Required fields:** |
19 | 19 |
|
20 |
- `blog_name` Your Tumblr URL (e.g. "mustardhamsters.tumblr.com") |
|
20 |
+ `blog_name` Your Tumblr URL (e.g. "mustardhamsters.tumblr.com") |
|
21 | 21 |
|
22 |
- `post_type` One of [text, photo, quote, link, chat, audio, video] |
|
22 |
+ `post_type` One of [text, photo, quote, link, chat, audio, video, reblog] |
|
23 | 23 |
|
24 | 24 |
|
25 | 25 |
------------- |
@@ -35,13 +35,13 @@ module Agents |
||
35 | 35 |
* `format` html, markdown |
36 | 36 |
* `slug` short text summary at end of the post URL |
37 | 37 |
|
38 |
- **Text** `title` `body` |
|
38 |
+ **Text** `title` `body` |
|
39 | 39 |
|
40 | 40 |
**Photo** `caption` `link` `source` |
41 | 41 |
|
42 | 42 |
**Quote** `quote` `source` |
43 | 43 |
|
44 |
- **Link** `title` `url` `description` |
|
44 |
+ **Link** `title` `url` `description` |
|
45 | 45 |
|
46 | 46 |
**Chat** `title` `conversation` |
47 | 47 |
|
@@ -49,6 +49,7 @@ module Agents |
||
49 | 49 |
|
50 | 50 |
**Video** `caption` `embed` |
51 | 51 |
|
52 |
+ **Reblog** `id` `reblog_key` `comment` |
|
52 | 53 |
|
53 | 54 |
------------- |
54 | 55 |
|
@@ -90,6 +91,9 @@ module Agents |
||
90 | 91 |
'conversation' => "{{conversation}}", |
91 | 92 |
'external_url' => "{{external_url}}", |
92 | 93 |
'embed' => "{{embed}}", |
94 |
+ 'id' => "{{id}}", |
|
95 |
+ 'reblog_key' => "{{reblog_key}}", |
|
96 |
+ 'comment' => "{{comment}}", |
|
93 | 97 |
}, |
94 | 98 |
} |
95 | 99 |
end |
@@ -105,19 +109,25 @@ module Agents |
||
105 | 109 |
options = interpolated(event)['options'] |
106 | 110 |
begin |
107 | 111 |
post = publish_post(blog_name, post_type, options) |
112 |
+ if !post.has_key?('id') |
|
113 |
+ log("Failed to create #{post_type} post on #{blog_name}: #{post.to_json}, options: #{options.to_json}") |
|
114 |
+ return |
|
115 |
+ end |
|
116 |
+ expanded_post = get_post(blog_name, post["id"]) |
|
108 | 117 |
create_event :payload => { |
109 | 118 |
'success' => true, |
110 | 119 |
'published_post' => "["+blog_name+"] "+post_type, |
111 | 120 |
'post_id' => post["id"], |
112 | 121 |
'agent_id' => event.agent_id, |
113 |
- 'event_id' => event.id |
|
122 |
+ 'event_id' => event.id, |
|
123 |
+ 'post' => expanded_post |
|
114 | 124 |
} |
115 | 125 |
end |
116 | 126 |
end |
117 | 127 |
end |
118 | 128 |
|
119 |
- def publish_post(blog_name, post_type, options) |
|
120 |
- options_obj = { |
|
129 |
+ def publish_post(blog_name, post_type, options) |
|
130 |
+ options_obj = { |
|
121 | 131 |
:state => options['state'], |
122 | 132 |
:tags => options['tags'], |
123 | 133 |
:tweet => options['tweet'], |
@@ -157,7 +167,19 @@ module Agents |
||
157 | 167 |
options_obj[:caption] = options['caption'] |
158 | 168 |
options_obj[:embed] = options['embed'] |
159 | 169 |
tumblr.video(blog_name, options_obj) |
170 |
+ when "reblog" |
|
171 |
+ options_obj[:id] = options['id'] |
|
172 |
+ options_obj[:reblog_key] = options['reblog_key'] |
|
173 |
+ options_obj[:comment] = options['comment'] |
|
174 |
+ tumblr.reblog(blog_name, options_obj) |
|
160 | 175 |
end |
161 | 176 |
end |
177 |
+ |
|
178 |
+ def get_post(blog_name, id) |
|
179 |
+ obj = tumblr.posts(blog_name, { |
|
180 |
+ :id => id |
|
181 |
+ }) |
|
182 |
+ obj["posts"].first |
|
183 |
+ end |
|
162 | 184 |
end |
163 | 185 |
end |
@@ -0,0 +1,105 @@ |
||
1 |
+module Agents |
|
2 |
+ class TwitterSearchAgent < Agent |
|
3 |
+ include TwitterConcern |
|
4 |
+ |
|
5 |
+ cannot_receive_events! |
|
6 |
+ |
|
7 |
+ description <<-MD |
|
8 |
+ The Twitter Search Agent performs and emits the results of a specified Twitter search. |
|
9 |
+ |
|
10 |
+ #{twitter_dependencies_missing if dependencies_missing?} |
|
11 |
+ |
|
12 |
+ If you want realtime data from Twitter about frequent terms, you should definitely use the Twitter Stream Agent instead. |
|
13 |
+ |
|
14 |
+ To be able to use this Agent you need to authenticate with Twitter in the [Services](/services) section first. |
|
15 |
+ |
|
16 |
+ You must provide the desired `search`. |
|
17 |
+ |
|
18 |
+ Set `result_type` to specify which [type of search results](https://dev.twitter.com/rest/reference/get/search/tweets) you would prefer to receive. Options are "mixed", "recent", and "popular". (default: `mixed`) |
|
19 |
+ |
|
20 |
+ Set `max_results` to limit the amount of results to retrieve per run(default: `500`. The API rate limit is ~18,000 per 15 minutes. [Click here to learn more about rate limits](https://dev.twitter.com/rest/public/rate-limiting). |
|
21 |
+ |
|
22 |
+ 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. |
|
23 |
+ |
|
24 |
+ Set `starting_at` to the date/time (eg. `Mon Jun 02 00:38:12 +0000 2014`) you want to start receiving tweets from (default: agent's `created_at`) |
|
25 |
+ MD |
|
26 |
+ |
|
27 |
+ event_description <<-MD |
|
28 |
+ Events are the raw JSON provided by the [Twitter API](https://dev.twitter.com/rest/reference/get/search/tweets). Should look something like: |
|
29 |
+ |
|
30 |
+ { |
|
31 |
+ ... every Tweet field, including ... |
|
32 |
+ "text": "something", |
|
33 |
+ "user": { |
|
34 |
+ "name": "Mr. Someone", |
|
35 |
+ "screen_name": "Someone", |
|
36 |
+ "location": "Vancouver BC Canada", |
|
37 |
+ "description": "...", |
|
38 |
+ "followers_count": 486, |
|
39 |
+ "friends_count": 1983, |
|
40 |
+ "created_at": "Mon Aug 29 23:38:14 +0000 2011", |
|
41 |
+ "time_zone": "Pacific Time (US & Canada)", |
|
42 |
+ "statuses_count": 3807, |
|
43 |
+ "lang": "en" |
|
44 |
+ }, |
|
45 |
+ "retweet_count": 0, |
|
46 |
+ "entities": ... |
|
47 |
+ "lang": "en" |
|
48 |
+ } |
|
49 |
+ MD |
|
50 |
+ |
|
51 |
+ default_schedule "every_1h" |
|
52 |
+ |
|
53 |
+ def working? |
|
54 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs? |
|
55 |
+ end |
|
56 |
+ |
|
57 |
+ def default_options |
|
58 |
+ { |
|
59 |
+ 'search' => 'freebandnames', |
|
60 |
+ 'expected_update_period_in_days' => '2' |
|
61 |
+ } |
|
62 |
+ end |
|
63 |
+ |
|
64 |
+ def validate_options |
|
65 |
+ errors.add(:base, "search is required") unless options['search'].present? |
|
66 |
+ errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present? |
|
67 |
+ |
|
68 |
+ if options[:starting_at].present? |
|
69 |
+ Time.parse(interpolated[:starting_at]) rescue errors.add(:base, "Error parsing starting_at") |
|
70 |
+ end |
|
71 |
+ end |
|
72 |
+ |
|
73 |
+ def starting_at |
|
74 |
+ if interpolated[:starting_at].present? |
|
75 |
+ Time.parse(interpolated[:starting_at]) rescue created_at |
|
76 |
+ else |
|
77 |
+ created_at |
|
78 |
+ end |
|
79 |
+ end |
|
80 |
+ |
|
81 |
+ def max_results |
|
82 |
+ (interpolated['max_results'].presence || 500).to_i |
|
83 |
+ end |
|
84 |
+ |
|
85 |
+ def check |
|
86 |
+ since_id = memory['since_id'] || nil |
|
87 |
+ opts = {include_entities: true} |
|
88 |
+ opts.merge! result_type: interpolated[:result_type] if interpolated[:result_type].present? |
|
89 |
+ opts.merge! since_id: since_id unless since_id.nil? |
|
90 |
+ |
|
91 |
+ # http://www.rubydoc.info/gems/twitter/Twitter/REST/Search |
|
92 |
+ tweets = twitter.search(interpolated['search'], opts).take(max_results) |
|
93 |
+ |
|
94 |
+ tweets.each do |tweet| |
|
95 |
+ if (tweet.created_at >= starting_at) |
|
96 |
+ memory['since_id'] = tweet.id if !memory['since_id'] || (tweet.id > memory['since_id']) |
|
97 |
+ |
|
98 |
+ create_event payload: tweet.attrs |
|
99 |
+ end |
|
100 |
+ end |
|
101 |
+ |
|
102 |
+ save! |
|
103 |
+ end |
|
104 |
+ end |
|
105 |
+end |
@@ -125,13 +125,13 @@ module Agents |
||
125 | 125 |
end |
126 | 126 |
|
127 | 127 |
def self.setup_worker |
128 |
- if Agents::TwitterStreamAgent.dependencies_missing? |
|
129 |
- STDERR.puts Agents::TwitterStreamAgent.twitter_dependencies_missing |
|
130 |
- STDERR.flush |
|
131 |
- return false |
|
132 |
- end |
|
133 |
- |
|
134 | 128 |
Agents::TwitterStreamAgent.active.group_by { |agent| agent.twitter_oauth_token }.map do |oauth_token, agents| |
129 |
+ if Agents::TwitterStreamAgent.dependencies_missing? |
|
130 |
+ STDERR.puts Agents::TwitterStreamAgent.twitter_dependencies_missing |
|
131 |
+ STDERR.flush |
|
132 |
+ return false |
|
133 |
+ end |
|
134 |
+ |
|
135 | 135 |
filter_to_agent_map = agents.map { |agent| agent.options[:filters] }.flatten.uniq.compact.map(&:strip).inject({}) { |m, f| m[f] = []; m } |
136 | 136 |
|
137 | 137 |
agents.each do |agent| |
@@ -176,7 +176,7 @@ module Agents |
||
176 | 176 |
|
177 | 177 |
def stop |
178 | 178 |
EventMachine.stop_event_loop if EventMachine.reactor_running? |
179 |
- thread.terminate |
|
179 |
+ terminate_thread! |
|
180 | 180 |
end |
181 | 181 |
|
182 | 182 |
private |
@@ -71,8 +71,16 @@ module Agents |
||
71 | 71 |
'expected_update_period_in_days' => '2' |
72 | 72 |
} |
73 | 73 |
end |
74 |
+ |
|
75 |
+ def check |
|
76 |
+ if key_setup? |
|
77 |
+ create_event :payload => model(weather_provider, which_day).merge('location' => location) |
|
78 |
+ end |
|
79 |
+ end |
|
74 | 80 |
|
75 |
- def service |
|
81 |
+ private |
|
82 |
+ |
|
83 |
+ def weather_provider |
|
76 | 84 |
interpolated["service"].presence || "wunderground" |
77 | 85 |
end |
78 | 86 |
|
@@ -85,8 +93,7 @@ module Agents |
||
85 | 93 |
end |
86 | 94 |
|
87 | 95 |
def validate_options |
88 |
- errors.add(:base, "service is required") unless service.present? |
|
89 |
- errors.add(:base, "service must be set to 'forecastio' or 'wunderground'") unless ["forecastio", "wunderground"].include?(service) |
|
96 |
+ errors.add(:base, "service must be set to 'forecastio' or 'wunderground'") unless ["forecastio", "wunderground"].include?(weather_provider) |
|
90 | 97 |
errors.add(:base, "location is required") unless location.present? |
91 | 98 |
errors.add(:base, "api_key is required") unless key_setup? |
92 | 99 |
errors.add(:base, "which_day selection is required") unless which_day.present? |
@@ -104,10 +111,10 @@ module Agents |
||
104 | 111 |
end |
105 | 112 |
end |
106 | 113 |
|
107 |
- def model(service,which_day) |
|
108 |
- if service == "wunderground" |
|
114 |
+ def model(weather_provider,which_day) |
|
115 |
+ if weather_provider == "wunderground" |
|
109 | 116 |
wunderground[which_day] |
110 |
- elsif service == "forecastio" |
|
117 |
+ elsif weather_provider == "forecastio" |
|
111 | 118 |
forecastio.each do |value| |
112 | 119 |
timestamp = Time.at(value.time) |
113 | 120 |
if (timestamp.to_date - Time.now.to_date).to_i == which_day |
@@ -174,12 +181,5 @@ module Agents |
||
174 | 181 |
end |
175 | 182 |
end |
176 | 183 |
end |
177 |
- |
|
178 |
- def check |
|
179 |
- if key_setup? |
|
180 |
- create_event :payload => model(service, which_day).merge('location' => location) |
|
181 |
- end |
|
182 |
- end |
|
183 |
- |
|
184 | 184 |
end |
185 | 185 |
end |
@@ -18,11 +18,12 @@ module Agents |
||
18 | 18 |
* `expected_receive_period_in_days` - How often you expect to receive |
19 | 19 |
events this way. Used to determine if the agent is working. |
20 | 20 |
* `payload_path` - JSONPath of the attribute in the POST body to be |
21 |
- used as the Event payload. If `payload_path` points to an array, |
|
22 |
- Events will be created for each element. |
|
21 |
+ used as the Event payload. Set to `.` to return the entire message. |
|
22 |
+ If `payload_path` points to an array, Events will be created for each element. |
|
23 | 23 |
* `verbs` - Comma-separated list of http verbs your agent will accept. |
24 | 24 |
For example, "post,get" will enable POST and GET requests. Defaults |
25 | 25 |
to "post". |
26 |
+ * `response` - The response message to the request. Defaults to 'Event Created'. |
|
26 | 27 |
MD |
27 | 28 |
end |
28 | 29 |
|
@@ -53,7 +54,7 @@ module Agents |
||
53 | 54 |
create_event(payload: payload) |
54 | 55 |
end |
55 | 56 |
|
56 |
- ['Event Created', 201] |
|
57 |
+ [response_message, 201] |
|
57 | 58 |
end |
58 | 59 |
|
59 | 60 |
def working? |
@@ -69,5 +70,9 @@ module Agents |
||
69 | 70 |
def payload_for(params) |
70 | 71 |
Utils.value_at(params, interpolated['payload_path']) || {} |
71 | 72 |
end |
73 |
+ |
|
74 |
+ def response_message |
|
75 |
+ interpolated['response'] || 'Event Created' |
|
76 |
+ end |
|
72 | 77 |
end |
73 | 78 |
end |
@@ -264,8 +264,9 @@ module Agents |
||
264 | 264 |
error "Ignoring a non-HTTP url: #{url.inspect}" |
265 | 265 |
return |
266 | 266 |
end |
267 |
- log "Fetching #{url}" |
|
268 |
- response = faraday.get(url) |
|
267 |
+ uri = Utils.normalize_uri(url) |
|
268 |
+ log "Fetching #{uri}" |
|
269 |
+ response = faraday.get(uri) |
|
269 | 270 |
raise "Failed: #{response.inspect}" unless response.success? |
270 | 271 |
|
271 | 272 |
interpolation_context.stack { |
@@ -303,7 +304,7 @@ module Agents |
||
303 | 304 |
interpolated['extract'].keys.each do |name| |
304 | 305 |
result[name] = output[name][index] |
305 | 306 |
if name.to_s == 'url' |
306 |
- result[name] = (response.env[:url] + result[name]).to_s |
|
307 |
+ result[name] = (response.env[:url] + Utils.normalize_uri(result[name])).to_s |
|
307 | 308 |
end |
308 | 309 |
end |
309 | 310 |
|
@@ -439,7 +440,14 @@ module Agents |
||
439 | 440 |
case nodes |
440 | 441 |
when Nokogiri::XML::NodeSet |
441 | 442 |
result = nodes.map { |node| |
442 |
- case value = node.xpath(extraction_details['value'] || '.') |
|
443 |
+ value = node.xpath(extraction_details['value'] || '.') |
|
444 |
+ if value.is_a?(Nokogiri::XML::NodeSet) |
|
445 |
+ child = value.first |
|
446 |
+ if child && child.cdata? |
|
447 |
+ value = child.text |
|
448 |
+ end |
|
449 |
+ end |
|
450 |
+ case value |
|
443 | 451 |
when Float |
444 | 452 |
# Node#xpath() returns any numeric value as float; |
445 | 453 |
# convert it to integer as appropriate. |
@@ -65,9 +65,8 @@ |
||
65 | 65 |
<div class="can-control-other-agents"> |
66 | 66 |
<div class="form-group"> |
67 | 67 |
<%= f.label :control_targets %> |
68 |
- <% eventControlTargets = current_user.agents.select(&:can_be_scheduled?) %> |
|
69 | 68 |
<%= f.select(:control_target_ids, |
70 |
- options_for_select(eventControlTargets.map {|s| [s.name, s.id] }, |
|
69 |
+ options_for_select(current_user.agents.map {|s| [s.name, s.id] }, |
|
71 | 70 |
@agent.control_target_ids), |
72 | 71 |
{}, { multiple: true, size: 5, class: 'select2 form-control' }) %> |
73 | 72 |
</div> |
@@ -53,7 +53,7 @@ |
||
53 | 53 |
</td> |
54 | 54 |
<td class='<%= "agent-unavailable" if agent.unavailable? %>'> |
55 | 55 |
<% if agent.can_create_events? %> |
56 |
- <%= link_to(agent.events_count || 0, agent_events_path(agent)) %> |
|
56 |
+ <%= link_to(agent.events_count || 0, agent_events_path(agent, return: (defined?(return_to) && return_to) || request.path)) %> |
|
57 | 57 |
<% else %> |
58 | 58 |
<span class='not-applicable'></span> |
59 | 59 |
<% end %> |
@@ -15,7 +15,7 @@ |
||
15 | 15 |
<li><a href="#logs" data-toggle="tab" data-agent-id="<%= @agent.id %>" class='<%= @agent.recent_error_logs? ? 'recent-errors' : '' %>'><span class='glyphicon glyphicon-list-alt'></span> Logs</a></li> |
16 | 16 |
|
17 | 17 |
<% if @agent.can_create_events? && @agent.events.count > 0 %> |
18 |
- <li><%= link_to icon_tag('glyphicon-random') + ' Events'.html_safe, agent_events_path(@agent) %></li> |
|
18 |
+ <li><%= link_to icon_tag('glyphicon-random') + ' Events'.html_safe, agent_events_path(@agent, return: request.fullpath) %></li> |
|
19 | 19 |
<% else %> |
20 | 20 |
<li class='disabled'><a><span class='glyphicon glyphicon-random'></span> Events</a></li> |
21 | 21 |
<% end %> |
@@ -20,7 +20,7 @@ |
||
20 | 20 |
</div> |
21 | 21 |
|
22 | 22 |
<div class='digraph'> |
23 |
- <%= render_agents_diagram(@agents) %> |
|
23 |
+ <%= render_agents_diagram(@agents, layout: params[:layout]) %> |
|
24 | 24 |
</div> |
25 | 25 |
</div> |
26 | 26 |
</div> |
@@ -40,7 +40,7 @@ |
||
40 | 40 |
|
41 | 41 |
<% if @agent %> |
42 | 42 |
<div class="btn-group"> |
43 |
- <%= link_to icon_tag('glyphicon-eye-open') + ' View Agent'.html_safe, agent_path(@agent, return: request.fullpath), class: "btn btn-default" %> |
|
43 |
+ <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, filtered_agent_return_link || agents_path, class: "btn btn-default" %> |
|
44 | 44 |
<%= link_to icon_tag('glyphicon-random') + ' See all events'.html_safe, events_path, class: "btn btn-default" %> |
45 | 45 |
</div> |
46 | 46 |
<% end %> |
@@ -16,4 +16,4 @@ end |
||
16 | 16 |
|
17 | 17 |
Delayed::Backend::ActiveRecord.configure do |config| |
18 | 18 |
config.reserve_sql_strategy = :default_sql |
19 |
-end |
|
19 |
+end |
@@ -6,6 +6,9 @@ development: |
||
6 | 6 |
enable_starttls_auto: <%= ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true' ? true : false %> |
7 | 7 |
user_name: <%= ENV['SMTP_USER_NAME'] || "" %> |
8 | 8 |
password: <%= ENV['SMTP_PASSWORD'] || "" %> |
9 |
+ openssl_verify_mode: <%= ENV['SMTP_OPENSSL_VERIFY_MODE'].presence || 'null' %> |
|
10 |
+ ca_path: <%= ENV['SMTP_OPENSSL_CA_PATH'].presence || 'null' %> |
|
11 |
+ ca_file: <%= ENV['SMTP_OPENSSL_CA_FILE'].presence || 'null' %> |
|
9 | 12 |
|
10 | 13 |
staging: |
11 | 14 |
address: <%= ENV['SMTP_SERVER'] || "smtp.gmail.com" %> |
@@ -15,6 +18,9 @@ staging: |
||
15 | 18 |
enable_starttls_auto: <%= ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true' ? true : false %> |
16 | 19 |
user_name: <%= ENV['SMTP_USER_NAME'] || "" %> |
17 | 20 |
password: <%= ENV['SMTP_PASSWORD'] || "" %> |
21 |
+ openssl_verify_mode: <%= ENV['SMTP_OPENSSL_VERIFY_MODE'].presence || 'null' %> |
|
22 |
+ ca_path: <%= ENV['SMTP_OPENSSL_CA_PATH'].presence || 'null' %> |
|
23 |
+ ca_file: <%= ENV['SMTP_OPENSSL_CA_FILE'].presence || 'null' %> |
|
18 | 24 |
|
19 | 25 |
production: |
20 | 26 |
address: <%= ENV['SMTP_SERVER'] || "smtp.gmail.com" %> |
@@ -23,4 +29,7 @@ production: |
||
23 | 29 |
authentication: <%= ENV['SMTP_AUTHENTICATION'] || "plain" %> |
24 | 30 |
enable_starttls_auto: <%= ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true' ? true : false %> |
25 | 31 |
user_name: <%= ENV['SMTP_USER_NAME'] || "" %> |
26 |
- password: <%= ENV['SMTP_PASSWORD'] || "" %> |
|
32 |
+ password: <%= ENV['SMTP_PASSWORD'] || "" %> |
|
33 |
+ openssl_verify_mode: <%= ENV['SMTP_OPENSSL_VERIFY_MODE'].presence || 'null' %> |
|
34 |
+ ca_path: <%= ENV['SMTP_OPENSSL_CA_PATH'].presence || 'null' %> |
|
35 |
+ ca_file: <%= ENV['SMTP_OPENSSL_CA_FILE'].presence || 'null' %> |
@@ -10,7 +10,7 @@ If you still wish to use the Heroku free plan (which won't work very well), plea |
||
10 | 10 |
|
11 | 11 |
* Heroku's [free plan](https://www.heroku.com/pricing) limits total runtime per day to 18 hours. This means that Huginn must sleep some of the time, and so recurring tasks will only run if their recurrence frequency fits within the free plan's awake time, which is 30 minutes. Therefore, we recommend that you only use the every 1 minute, every 2 minute, and every 5 minute Agent scheduling options. |
12 | 12 |
* If you're using the free plan, you need to signup for a free [uptimerobot](https://uptimerobot.com) account and have it ping your Huginn URL on Heroku once every 70 minutes. If you still receive warnings from Heroku, try a longer interval. |
13 |
-* Heroku's free Postgres plan limits the number of database rows that you can have to 10,000, so you should be sure to set a low event retention schedule for your agents. |
|
13 |
+* Heroku's free Postgres plan limits the number of database rows that you can have to 10,000, so you should be sure to set a low event retention schedule for your agents and set `AGENT_LOG_LENGTH`, the number of log lines kept in the DB per Agent, to something small: `heroku config:set AGENT_LOG_LENGTH=20`. |
|
14 | 14 |
|
15 | 15 |
## Instructions |
16 | 16 |
|
@@ -100,7 +100,7 @@ class AgentRunner |
||
100 | 100 |
|
101 | 101 |
def restart_dead_workers |
102 | 102 |
@workers.each_pair do |id, worker| |
103 |
- if worker.thread && !worker.thread.alive? |
|
103 |
+ if !worker.restarting && worker.thread && !worker.thread.alive? |
|
104 | 104 |
puts "Restarting #{id.to_s}" |
105 | 105 |
@workers[id].run! |
106 | 106 |
end |
@@ -61,7 +61,7 @@ class Rufus::Scheduler |
||
61 | 61 |
job.scheduler_agent_id = agent_id |
62 | 62 |
|
63 | 63 |
if scheduler_agent = job.scheduler_agent |
64 |
- scheduler_agent.check! |
|
64 |
+ scheduler_agent.control! |
|
65 | 65 |
else |
66 | 66 |
puts "Unscheduling SchedulerAgent##{job.scheduler_agent_id} (disabled or deleted)" |
67 | 67 |
job.unschedule |
@@ -1,6 +1,9 @@ |
||
1 | 1 |
class JSONWithIndifferentAccess |
2 | 2 |
def self.load(json) |
3 | 3 |
ActiveSupport::HashWithIndifferentAccess.new(JSON.parse(json || '{}')) |
4 |
+ rescue JSON::ParserError |
|
5 |
+ Rails.logger.error "Unparsable JSON in JSONWithIndifferentAccess: #{json}" |
|
6 |
+ { 'error' => 'unparsable json detected during de-serialization' } |
|
4 | 7 |
end |
5 | 8 |
|
6 | 9 |
def self.dump(hash) |
@@ -38,6 +38,11 @@ namespace :production do |
||
38 | 38 |
run_sv('start') |
39 | 39 |
end |
40 | 40 |
|
41 |
+ task :force_stop => :check do |
|
42 |
+ puts "Force stopping huginn ..." |
|
43 |
+ run_sv('force-stop') |
|
44 |
+ end |
|
45 |
+ |
|
41 | 46 |
task :status => :check do |
42 | 47 |
run_sv('status') |
43 | 48 |
end |
@@ -91,4 +96,4 @@ rescue StandardError => e |
||
91 | 96 |
raise e |
92 | 97 |
else |
93 | 98 |
puts output |
94 |
-end |
|
99 |
+end |
@@ -21,6 +21,18 @@ module Utils |
||
21 | 21 |
end |
22 | 22 |
end |
23 | 23 |
|
24 |
+ def self.normalize_uri(uri) |
|
25 |
+ begin |
|
26 |
+ URI(uri) |
|
27 |
+ rescue URI::Error |
|
28 |
+ URI(uri.to_s.gsub(/[^\-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]+/) { |unsafe| |
|
29 |
+ unsafe.bytes.each_with_object(String.new) { |uc, s| |
|
30 |
+ s << sprintf('%%%02X', uc) |
|
31 |
+ } |
|
32 |
+ }.force_encoding(Encoding::US_ASCII)) |
|
33 |
+ end |
|
34 |
+ end |
|
35 |
+ |
|
24 | 36 |
def self.interpolate_jsonpaths(value, data, options = {}) |
25 | 37 |
if options[:leading_dollarsign_is_jsonpath] && value[0] == '$' |
26 | 38 |
Utils.values_at(data, value).first.to_s |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe DryRunnable do |
4 | 4 |
class Agents::SandboxedAgent < Agent |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe FormConfigurable do |
4 | 4 |
class Agent1 |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
require 'inheritance_tracking' |
3 | 3 |
|
4 | 4 |
describe InheritanceTracking do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe LiquidDroppable do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
require 'nokogiri' |
3 | 3 |
|
4 | 4 |
describe LiquidInterpolatable::Filters do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe LongRunnable do |
4 | 4 |
class LongRunnableAgent < Agent |
@@ -76,6 +76,14 @@ describe LongRunnable do |
||
76 | 76 |
context "#stop!" do |
77 | 77 |
it "terminates the thread" do |
78 | 78 |
mock(@worker.thread).terminate |
79 |
+ mock(@worker.thread).status { 'run' } |
|
80 |
+ @worker.stop! |
|
81 |
+ end |
|
82 |
+ |
|
83 |
+ it "wakes up sleeping threads after termination" do |
|
84 |
+ mock(@worker.thread).terminate |
|
85 |
+ mock(@worker.thread).status { 'sleep' } |
|
86 |
+ mock(@worker.thread).wakeup |
|
79 | 87 |
@worker.stop! |
80 | 88 |
end |
81 | 89 |
|
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe SortableEvents do |
4 | 4 |
let(:agent_class) { |
@@ -152,7 +152,7 @@ describe SortableEvents do |
||
152 | 152 |
passive_agent_class.class_eval do |
153 | 153 |
can_order_created_events! |
154 | 154 |
end |
155 |
- }.to raise_error |
|
155 |
+ }.to raise_error('Cannot order events for agent that cannot create events') |
|
156 | 156 |
end |
157 | 157 |
|
158 | 158 |
it 'should work if called from an Agent that can create events' do |
@@ -160,7 +160,7 @@ describe SortableEvents do |
||
160 | 160 |
active_agent_class.class_eval do |
161 | 161 |
can_order_created_events! |
162 | 162 |
end |
163 |
- }.not_to raise_error |
|
163 |
+ }.not_to raise_error() |
|
164 | 164 |
end |
165 | 165 |
end |
166 | 166 |
|
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe AgentsController do |
4 | 4 |
def valid_attributes(options = {}) |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe SortableTable do |
4 | 4 |
class SortableTestController |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe EventsController do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe JobsController do |
4 | 4 |
describe "GET index" do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe LogsController do |
4 | 4 |
describe "GET index" do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe OmniauthCallbacksController do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe ScenarioImportsController do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe ScenariosController do |
4 | 4 |
def valid_attributes(options = {}) |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe ServicesController do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe UserCredentialsController do |
4 | 4 |
def valid_attributes(options = {}) |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe WebRequestsController do |
4 | 4 |
class Agents::WebRequestReceiverAgent < Agent |
@@ -0,0 +1,131 @@ |
||
1 |
+<?xml version="1.0" encoding="UTF-8"?> |
|
2 |
+<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-gb"> |
|
3 |
+ <link rel="self" type="application/atom+xml" href="http://rainmeter.net/forum/feed.php" /> |
|
4 |
+ <title>Rainmeter Forums</title> |
|
5 |
+ <link href="http://rainmeter.net/forum/index.php" /> |
|
6 |
+ <updated>2015-10-11T14:52:35+00:00</updated> |
|
7 |
+ <author> |
|
8 |
+ <name><![CDATA[Rainmeter Forums]]></name> |
|
9 |
+ </author> |
|
10 |
+ <id>http://rainmeter.net/forum/feed.php</id> |
|
11 |
+ <entry> |
|
12 |
+ <author> |
|
13 |
+ <name><![CDATA[supergergo]]></name> |
|
14 |
+ </author> |
|
15 |
+ <updated>2015-10-11T14:52:35+00:00</updated> |
|
16 |
+ <published>2015-10-11T14:52:35+00:00</published> |
|
17 |
+ <id>http://rainmeter.net/forum/viewtopic.php?t=21984&p=116390#p116390</id> |
|
18 |
+ <link href="http://rainmeter.net/forum/viewtopic.php?t=21984&p=116390#p116390" /> |
|
19 |
+ <title type="html"><![CDATA[Help: Rainmeter Skins • Re: Cannot change font for Simple Launcher]]></title> |
|
20 |
+ <category term="Help: Rainmeter Skins" scheme="http://rainmeter.net/forum/viewforum.php?f=5" label="Help: Rainmeter Skins" /> |
|
21 |
+ <content type="html" xml:base="http://rainmeter.net/forum/viewtopic.php?t=21984&p=116390#p116390"><![CDATA[<div class="quotewrapper"><div class="quotetitle">jsmorley wrote:</div><div class="quotecontent"><br />You don't use the file name of the font, you use the internal font name.<br /><br /><span style="background: none repeat scroll 0 0 #FFFFFF; border: 1px solid #869AA8; color: #2E8B57; display: inline; font-family: Monaco,'Andale Mono','Courier New',Courier,monospace; font-size: 1em; font-style: normal; line-height: 1.3em; padding: 0 3px;">FontFace=ITC Avant Garde Pro XLt</span><br /><br /><div class="attachwrapper">1.jpg</div><br /></div></div><br /><br />Thank you!!<p>Statistics: Posted by <a href="http://rainmeter.net/forum/memberlist.php?mode=viewprofile&u=35297">supergergo</a> — 29 minutes ago</p><hr />]]></content> |
|
22 |
+ </entry> |
|
23 |
+ <entry> |
|
24 |
+ <author> |
|
25 |
+ <name><![CDATA[redsaph]]></name> |
|
26 |
+ </author> |
|
27 |
+ <updated>2015-10-11T13:51:44+00:00</updated> |
|
28 |
+ <published>2015-10-11T13:51:44+00:00</published> |
|
29 |
+ <id>http://rainmeter.net/forum/viewtopic.php?t=21411&p=116389#p116389</id> |
|
30 |
+ <link href="http://rainmeter.net/forum/viewtopic.php?t=21411&p=116389#p116389" /> |
|
31 |
+ <title type="html"><![CDATA[Plugins & Addons • Re: Plugin: ColorExtract]]></title> |
|
32 |
+ <category term="Plugins & Addons" scheme="http://rainmeter.net/forum/viewforum.php?f=18" label="Plugins & Addons" /> |
|
33 |
+ <content type="html" xml:base="http://rainmeter.net/forum/viewtopic.php?t=21411&p=116389#p116389"><![CDATA[This is pretty great! The things we can do with this plugin... <img src="http://rainmeter.net/forum/images/smilies/yikes.gif" alt=":o" title="Shocked" /><p>Statistics: Posted by <a href="http://rainmeter.net/forum/memberlist.php?mode=viewprofile&u=28432">redsaph</a> — Today, 1:51 pm</p><hr />]]></content> |
|
34 |
+ </entry> |
|
35 |
+ <entry> |
|
36 |
+ <author> |
|
37 |
+ <name><![CDATA[jsmorley]]></name> |
|
38 |
+ </author> |
|
39 |
+ <updated>2015-10-11T13:36:51+00:00</updated> |
|
40 |
+ <published>2015-10-11T13:36:51+00:00</published> |
|
41 |
+ <id>http://rainmeter.net/forum/viewtopic.php?t=21984&p=116388#p116388</id> |
|
42 |
+ <link href="http://rainmeter.net/forum/viewtopic.php?t=21984&p=116388#p116388" /> |
|
43 |
+ <title type="html"><![CDATA[Help: Rainmeter Skins • Re: Cannot change font for Simple Launcher]]></title> |
|
44 |
+ <category term="Help: Rainmeter Skins" scheme="http://rainmeter.net/forum/viewforum.php?f=5" label="Help: Rainmeter Skins" /> |
|
45 |
+ <content type="html" xml:base="http://rainmeter.net/forum/viewtopic.php?t=21984&p=116388#p116388"><![CDATA[You don't use the file name of the font, you use the internal font name.<br /><br /><span style="background: none repeat scroll 0 0 #FFFFFF; border: 1px solid #869AA8; color: #2E8B57; display: inline; font-family: Monaco,'Andale Mono','Courier New',Courier,monospace; font-size: 1em; font-style: normal; line-height: 1.3em; padding: 0 3px;">FontFace=ITC Avant Garde Pro XLt</span><br /><br /><div class="attachwrapper">1.jpg</div><p>Statistics: Posted by <a href="http://rainmeter.net/forum/memberlist.php?mode=viewprofile&u=85">jsmorley</a> — Today, 1:36 pm</p><hr />]]></content> |
|
46 |
+ </entry> |
|
47 |
+ <entry> |
|
48 |
+ <author> |
|
49 |
+ <name><![CDATA[xlr8r_]]></name> |
|
50 |
+ </author> |
|
51 |
+ <updated>2015-10-11T13:35:15+00:00</updated> |
|
52 |
+ <published>2015-10-11T13:35:15+00:00</published> |
|
53 |
+ <id>http://rainmeter.net/forum/viewtopic.php?t=21939&p=116387#p116387</id> |
|
54 |
+ <link href="http://rainmeter.net/forum/viewtopic.php?t=21939&p=116387#p116387" /> |
|
55 |
+ <title type="html"><![CDATA[Tips & Tricks • Re: Working with the HWiNFO plugin]]></title> |
|
56 |
+ <category term="Tips & Tricks" scheme="http://rainmeter.net/forum/viewforum.php?f=15" label="Tips & Tricks" /> |
|
57 |
+ <content type="html" xml:base="http://rainmeter.net/forum/viewtopic.php?t=21939&p=116387#p116387"><![CDATA[again: one more<p>Statistics: Posted by <a href="http://rainmeter.net/forum/memberlist.php?mode=viewprofile&u=35276">xlr8r_</a> — Today, 1:35 pm</p><hr />]]></content> |
|
58 |
+ </entry> |
|
59 |
+ <entry> |
|
60 |
+ <author> |
|
61 |
+ <name><![CDATA[supergergo]]></name> |
|
62 |
+ </author> |
|
63 |
+ <updated>2015-10-11T13:27:56+00:00</updated> |
|
64 |
+ <published>2015-10-11T13:27:56+00:00</published> |
|
65 |
+ <id>http://rainmeter.net/forum/viewtopic.php?t=21984&p=116386#p116386</id> |
|
66 |
+ <link href="http://rainmeter.net/forum/viewtopic.php?t=21984&p=116386#p116386" /> |
|
67 |
+ <title type="html"><![CDATA[Help: Rainmeter Skins • Cannot change font for Simple Launcher]]></title> |
|
68 |
+ <category term="Help: Rainmeter Skins" scheme="http://rainmeter.net/forum/viewforum.php?f=5" label="Help: Rainmeter Skins" /> |
|
69 |
+ <content type="html" xml:base="http://rainmeter.net/forum/viewtopic.php?t=21984&p=116386#p116386"><![CDATA[Hi,<br />I simply cannot change the font for this skin. I tried the @Resources folder in the skin's root, but nothing, I also tried to install the font, but still, nothing. <br /><br />Here is a snip of my code:<br />[APP1]<br />Meter=STRING<br />X=0<br />Y=0<br />FontColor=255, 255, 255, 1000<br />FontSize=40<br />FontFace=ITCAvantGardePro-XLt<br />SolidColor=0,0,0,1<br />StringStyle=NORMAL<br />StringAlign=LEFT<br />AntiAlias=1<br />Text="University"<br />LeftMouseUpAction=!Execute ["C:\Users\G\Documents\Uni"]<br /><br />I also tried to use other variations of the font, but still nothing happens when I save and hit refresh. Even if it says the font's name at FontFace, it does not change.<br /><br />Link for the launcher: <!-- m --><a class="postlink" href="http://danieliop.deviantart.com/art/Simple-RM-Launcher-216478809">http://danieliop.deviantart.com/art/Sim ... -216478809</a><!-- m --><p>Statistics: Posted by <a href="http://rainmeter.net/forum/memberlist.php?mode=viewprofile&u=35297">supergergo</a> — Today, 1:27 pm</p><hr />]]></content> |
|
70 |
+ </entry> |
|
71 |
+ <entry> |
|
72 |
+ <author> |
|
73 |
+ <name><![CDATA[balala]]></name> |
|
74 |
+ </author> |
|
75 |
+ <updated>2015-10-11T08:27:55+00:00</updated> |
|
76 |
+ <published>2015-10-11T08:27:55+00:00</published> |
|
77 |
+ <id>http://rainmeter.net/forum/viewtopic.php?t=21982&p=116381#p116381</id> |
|
78 |
+ <link href="http://rainmeter.net/forum/viewtopic.php?t=21982&p=116381#p116381" /> |
|
79 |
+ <title type="html"><![CDATA[Help: Rainmeter Skins • Re: Test if Today is Between 2 Dates]]></title> |
|
80 |
+ <category term="Help: Rainmeter Skins" scheme="http://rainmeter.net/forum/viewforum.php?f=5" label="Help: Rainmeter Skins" /> |
|
81 |
+ <content type="html" xml:base="http://rainmeter.net/forum/viewtopic.php?t=21982&p=116381#p116381"><![CDATA[If I'm not wrong your bangs are wrong, because you want to show/hide a group of meters. For that, you have to use the !ShowMeterGroup/!HideMeterGroup bangs, instead of !ShowGroup/!HideGroup. The last pair will show/hide a group of active skins, while the !ShowMeterGroup/!HideMeterGroup will show/hide a group of meters. On your posted code, One is a group of meters (it contains the [Boo] meter). So, replace the [MeasureDate] measure with something like this:<br /><div><div class="codetitle"><b>Code:</b> </div><div class="codecontent"><code>[MeasureDate]<br />Measure=Time<br />Format=%#m.%d<br />IfCondition=(MeasureDate >= 12.02) && (MeasureDate <= 12.25)<br />IfTrueAction=[!ShowMeterGroup One]<br />IfFalseAction=[!HideMeterGroup One]</code></div></div><p>Statistics: Posted by <a href="http://rainmeter.net/forum/memberlist.php?mode=viewprofile&u=7491">balala</a> — Today, 8:27 am</p><hr />]]></content> |
|
82 |
+ </entry> |
|
83 |
+ <entry> |
|
84 |
+ <author> |
|
85 |
+ <name><![CDATA[bill98]]></name> |
|
86 |
+ </author> |
|
87 |
+ <updated>2015-10-11T14:02:43+00:00</updated> |
|
88 |
+ <published>2015-10-11T07:36:57+00:00</published> |
|
89 |
+ <id>http://rainmeter.net/forum/viewtopic.php?t=21982&p=116380#p116380</id> |
|
90 |
+ <link href="http://rainmeter.net/forum/viewtopic.php?t=21982&p=116380#p116380" /> |
|
91 |
+ <title type="html"><![CDATA[Help: Rainmeter Skins • Re: Test if Today is Between 2 Dates]]></title> |
|
92 |
+ <category term="Help: Rainmeter Skins" scheme="http://rainmeter.net/forum/viewforum.php?f=5" label="Help: Rainmeter Skins" /> |
|
93 |
+ <content type="html" xml:base="http://rainmeter.net/forum/viewtopic.php?t=21982&p=116380#p116380"><![CDATA[Thanks! I must have something else wrong, because it always shows the group/Image.<br /><br /><div><div class="codetitle"><b>Code:</b> </div><div class="codecontent"><code>[Variables]<br />@Include1=#@#VariablesM2.inc<br />@Include2=#@#FloatingImage.inc<br />IN=1<br />;Holiday<br /><br />[mIName]<br />Measure=String<br />String=#IN#<br />DynamicVariables=1<br />RegExpSubstitute=1<br />Substitute="^1$":"#1IName#","^2$":"#2IName#","^3$":"#3IName#","^4$":"#4IName#","^5$":"#5IName#","^6$":"#6IName#","^7$":"#7IName#","^8$":"#8IName#","^9$":"#9IName#","^10$":"#10IName#"<br /><br />[mSizex]<br />Measure=String<br />String=#IN#<br />DynamicVariables=1<br />RegExpSubstitute=1<br />Substitute="^1$":"#1Sizex#","^2$":"#2Sizex#","^3$":"#3Sizex#","^4$":"#4Sizex#","^5$":"#5Sizex#","^6$":"#6Sizex#","^7$":"#7Sizex#","^8$":"#8Sizex#","^9$":"#9Sizex#","^10$":"#10Sizex#"<br /><br />[mSizey]<br />Measure=String<br />String=#IN#<br />DynamicVariables=1<br />RegExpSubstitute=1<br />Substitute="^1$":"#1Sizey#","^2$":"#2Sizey#","^3$":"#3Sizey#","^4$":"#4Sizey#","^5$":"#5Sizey#","^6$":"#6Sizey#","^7$":"#7Sizey#","^8$":"#8Sizey#","^9$":"#9Sizey#","^10$":"#10Sizey#"<br /><br />[mXMovemt]<br />Measure=String<br />String=#IN#<br />DynamicVariables=1<br />RegExpSubstitute=1<br />Substitute="^1$":"#1XMovemt#","^2$":"#2XMovemt#","^3$":"#3XMovemt#","^4$":"#4XMovemt#","^5$":"#5XMovemt#","^6$":"#6XMovemt#","^7$":"#7XMovemt#","^8$":"#8XMovemt#","^9$":"#9XMovemt#","^10$":"#10XMovemt#"<br /><br />[mYMovemt]<br />Measure=String<br />String=#IN#<br />DynamicVariables=1<br />RegExpSubstitute=1<br />Substitute="^1$":"#1YMovemt#","^2$":"#2YMovemt#","^3$":"#3YMovemt#","^4$":"#4YMovemt#","^5$":"#5YMovemt#","^6$":"#6YMovemt#","^7$":"#7YMovemt#","^8$":"#8YMovemt#","^9$":"#9YMovemt#","^10$":"#10YMovemt#"<br /><br />[mUpDDiv]<br />Measure=String<br />String=#IN#<br />DynamicVariables=1<br />RegExpSubstitute=1<br />Substitute="^1$":"#1UpDDiv#","^2$":"#2UpDDiv#","^3$":"#3UpDDiv#","^4$":"#4UpDDiv#","^5$":"#5UpDDiv#","^6$":"#6UpDDiv#","^7$":"#7UpDDiv#","^8$":"#8UpDDiv#","^9$":"#9UpDDiv#","^10$":"#10UpDDiv#"<br /><br />[MeasureDate]<br />Measure=Time<br />Format=%#m.%d<br />IfCondition=(MeasureDate >= 12.02) && (MeasureDate <= 12.25)<br />IfTrueAction=[!ShowGroup One]<br />IfFalseAction=[!HideGroup One]<br /><br /><br />[RX]<br />Measure=Calc<br />Formula=Random<br />UpdateRandom=1<br />LowBound=(#Xc#-([mSizex]+[mXMovemt]))<br />HighBound=(#Xc#+[mXMovemt])<br />DynamicVariables=1<br />UpdateDivider=[mUpDDiv]<br /><br /><br />[RY]<br />Measure=Calc<br />Formula=Random<br />UpdateRandom=1<br />LowBound=(#Yc#-([mSizey]+[mYMovemt]))<br />HighBound=(#Yc#+[mYMovemt])<br />DynamicVariables=1<br />UpdateDivider=[mUpDDiv]<br /><br />[Boo]<br />Meter=Image<br />ImageName=#@#icons\[mIName]<br />x=[RX]<br />Y=[RY]<br />DynamicVariables=1<br />Group=One<br /><br /><br />[SHData]<br />Meter=String<br />x=900<br />y=50<br />FontSize=15<br />FontColor=0,0,0,255<br />Text=Xc=#Xc##CRLF#Yc=#Yc##CRLF#RX=[RX]#CRLF#RY=[RY]#CRLF#Xmovemt=[mXmovemt]#CRLF#YMovemt=[mYMovemt]#CRLF#UpDDivider=[mUpDDiv]#CRLF#[MeasureDate]</code></div></div><p>Statistics: Posted by <a href="http://rainmeter.net/forum/memberlist.php?mode=viewprofile&u=10741">bill98</a> — Today, 7:36 am</p><hr />]]></content> |
|
94 |
+ </entry> |
|
95 |
+ <entry> |
|
96 |
+ <author> |
|
97 |
+ <name><![CDATA[balala]]></name> |
|
98 |
+ </author> |
|
99 |
+ <updated>2015-10-11T05:42:06+00:00</updated> |
|
100 |
+ <published>2015-10-11T05:42:06+00:00</published> |
|
101 |
+ <id>http://rainmeter.net/forum/viewtopic.php?t=21980&p=116379#p116379</id> |
|
102 |
+ <link href="http://rainmeter.net/forum/viewtopic.php?t=21980&p=116379#p116379" /> |
|
103 |
+ <title type="html"><![CDATA[Help: Rainmeter Skins • Re: Lano Visualizer - computer can't auto-sleep]]></title> |
|
104 |
+ <category term="Help: Rainmeter Skins" scheme="http://rainmeter.net/forum/viewforum.php?f=5" label="Help: Rainmeter Skins" /> |
|
105 |
+ <content type="html" xml:base="http://rainmeter.net/forum/viewtopic.php?t=21980&p=116379#p116379"><![CDATA[I had the same issue a while ago. I also looked for a fix/solution, but found no one. Finaly I stoped using any visualizer skin. But I also would be interested finding a fix, if it's possible. There is one?<p>Statistics: Posted by <a href="http://rainmeter.net/forum/memberlist.php?mode=viewprofile&u=7491">balala</a> — Today, 5:42 am</p><hr />]]></content> |
|
106 |
+ </entry> |
|
107 |
+ <entry> |
|
108 |
+ <author> |
|
109 |
+ <name><![CDATA[balala]]></name> |
|
110 |
+ </author> |
|
111 |
+ <updated>2015-10-11T05:37:02+00:00</updated> |
|
112 |
+ <published>2015-10-11T05:37:02+00:00</published> |
|
113 |
+ <id>http://rainmeter.net/forum/viewtopic.php?t=21982&p=116378#p116378</id> |
|
114 |
+ <link href="http://rainmeter.net/forum/viewtopic.php?t=21982&p=116378#p116378" /> |
|
115 |
+ <title type="html"><![CDATA[Help: Rainmeter Skins • Re: Test if Today is Between 2 Dates]]></title> |
|
116 |
+ <category term="Help: Rainmeter Skins" scheme="http://rainmeter.net/forum/viewforum.php?f=5" label="Help: Rainmeter Skins" /> |
|
117 |
+ <content type="html" xml:base="http://rainmeter.net/forum/viewtopic.php?t=21982&p=116378#p116378"><![CDATA[Don't use the IfMatch/IfMatchAction/IfNotMatchAction options, instead try with IfConditions. To do that, you have to properly format the date. Instead of the <span style="background: none repeat scroll 0 0 #FFFFFF; border: 1px solid #869AA8; color: #2E8B57; display: inline; font-family: Monaco,'Andale Mono','Courier New',Courier,monospace; font-size: 1em; font-style: normal; line-height: 1.3em; padding: 0 3px;">Format=%m%d</span>, try this format option: <span style="background: none repeat scroll 0 0 #FFFFFF; border: 1px solid #869AA8; color: #2E8B57; display: inline; font-family: Monaco,'Andale Mono','Courier New',Courier,monospace; font-size: 1em; font-style: normal; line-height: 1.3em; padding: 0 3px;">Format=%m.%d</span>. Now you'll have a date formated as a decimal number (eg for today I have right now 10.11). This way you can use the IfConditions:<br /><div><div class="codetitle"><b>Code:</b> </div><div class="codecontent"><code>[MeasureDate]<br />Measure=Time<br />Format=%#m.%d<br />IfCondition=((MeasureDate>=10.01)&&(MeasureDate<=10.09))<br />IfTrueAction=[!ShowGroup One]<br />IfFalseAction=[!HideGroup One]</code></div></div><br />Two other comments about your code:<br />1. I don't think you'd need the <span style="background: none repeat scroll 0 0 #FFFFFF; border: 1px solid #869AA8; color: #2E8B57; display: inline; font-family: Monaco,'Andale Mono','Courier New',Courier,monospace; font-size: 1em; font-style: normal; line-height: 1.3em; padding: 0 3px;">IfConditionMode=1</span>/<span style="background: none repeat scroll 0 0 #FFFFFF; border: 1px solid #869AA8; color: #2E8B57; display: inline; font-family: Monaco,'Andale Mono','Courier New',Courier,monospace; font-size: 1em; font-style: normal; line-height: 1.3em; padding: 0 3px;">IfMatchMode=1</span> option. Without it, the condition will be checked when the date is changing, with it, on every update cycle. It's useless.<br />2. Even if you'd try to use the IfMatch option, in your code that was wrong: you can't use operators (=, < or >), because we're talking about strings.<p>Statistics: Posted by <a href="http://rainmeter.net/forum/memberlist.php?mode=viewprofile&u=7491">balala</a> — Today, 5:37 am</p><hr />]]></content> |
|
118 |
+ </entry> |
|
119 |
+ <entry> |
|
120 |
+ <author> |
|
121 |
+ <name><![CDATA[bill98]]></name> |
|
122 |
+ </author> |
|
123 |
+ <updated>2015-10-11T03:38:51+00:00</updated> |
|
124 |
+ <published>2015-10-11T03:38:51+00:00</published> |
|
125 |
+ <id>http://rainmeter.net/forum/viewtopic.php?t=21982&p=116377#p116377</id> |
|
126 |
+ <link href="http://rainmeter.net/forum/viewtopic.php?t=21982&p=116377#p116377" /> |
|
127 |
+ <title type="html"><![CDATA[Help: Rainmeter Skins • Test if Today is Between 2 Dates]]></title> |
|
128 |
+ <category term="Help: Rainmeter Skins" scheme="http://rainmeter.net/forum/viewforum.php?f=5" label="Help: Rainmeter Skins" /> |
|
129 |
+ <content type="html" xml:base="http://rainmeter.net/forum/viewtopic.php?t=21982&p=116377#p116377"><![CDATA[Can I do something like this to test if the date is between two dates<br /><br /><br />[MeasureDate]<br />Measure=Time<br />Format=%m%d<br />IfMatch>=1001 && <=1009<br />IfMatchAction=[!ShowGroup One]<br />IfNotMatchAction=[!HideGroup One]<br />IfMatchMode=1<p>Statistics: Posted by <a href="http://rainmeter.net/forum/memberlist.php?mode=viewprofile&u=10741">bill98</a> — Today, 3:38 am</p><hr />]]></content> |
|
130 |
+ </entry> |
|
131 |
+</feed> |
@@ -0,0 +1 @@ |
||
1 |
+{"statuses":[{"metadata":{"result_type":"recent","iso_language_code":"en"},"created_at":"Fri Dec 20 16:52:25 +0000 2013","id":414075829182676992,"id_str":"414075829182676992","text":"@Just_Reboot #FreeBandNames mono surround","source":"\u003ca href=\"http:\/\/www.myplume.com\/\" rel=\"nofollow\"\u003ePlume\u00a0for\u00a0Android\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":414069174856843264,"in_reply_to_status_id_str":"414069174856843264","in_reply_to_user_id":29296581,"in_reply_to_user_id_str":"29296581","in_reply_to_screen_name":"Just_Reboot","user":{"id":546527520,"id_str":"546527520","name":"Phil Empanada","screen_name":"ItsFuckinOhSo","location":"By cacti and sand","description":"Insert cheesy warnings about this account","url":null,"entities":{"description":{"urls":[]}},"protected":false,"followers_count":257,"friends_count":347,"listed_count":10,"created_at":"Fri Apr 06 02:15:16 +0000 2012","favourites_count":345,"utc_offset":-25200,"time_zone":"Arizona","geo_enabled":false,"verified":false,"statuses_count":21765,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/414512901680930816\/AcmN-ByT_normal.jpeg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/414512901680930816\/AcmN-ByT_normal.jpeg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/546527520\/1371244104","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"favorite_count":0,"entities":{"hashtags":[{"text":"FreeBandNames","indices":[13,27]}],"symbols":[],"urls":[],"user_mentions":[{"screen_name":"Just_Reboot","name":"Reboot","id":29296581,"id_str":"29296581","indices":[0,12]}]},"favorited":false,"retweeted":false,"lang":"en"},{"metadata":{"result_type":"recent","iso_language_code":"en"},"created_at":"Fri Dec 20 16:43:32 +0000 2013","id":414073595372265472,"id_str":"414073595372265472","text":"RT @Just_Reboot: The Dick Tonsils #FreeBandNames\u00a0","source":"\u003ca href=\"http:\/\/twitter.com\/download\/android\" rel=\"nofollow\"\u003eTwitter for Android\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":109670150,"id_str":"109670150","name":"Derek Rodriguez","screen_name":"drod2169","location":"Tampa, Florida","description":"Web Developer, College Student, Workout Addict, Taco Fiend. #FBGT","url":null,"entities":{"description":{"urls":[]}},"protected":false,"followers_count":3119,"friends_count":714,"listed_count":142,"created_at":"Fri Jan 29 21:29:16 +0000 2010","favourites_count":3728,"utc_offset":-18000,"time_zone":"Eastern Time (US & Canada)","geo_enabled":false,"verified":false,"statuses_count":46991,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/3347464379\/1046c92ab1834f1a3c1692ea585a1d69_normal.jpeg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/3347464379\/1046c92ab1834f1a3c1692ea585a1d69_normal.jpeg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/109670150\/1368729387","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweeted_status":{"metadata":{"result_type":"recent","iso_language_code":"en"},"created_at":"Fri Dec 20 16:34:40 +0000 2013","id":414071361066532864,"id_str":"414071361066532864","text":"The Dick Tonsils #FreeBandNames\u00a0","source":"\u003ca href=\"http:\/\/www.myplume.com\/\" rel=\"nofollow\"\u003ePlume\u00a0for\u00a0Android\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":29296581,"id_str":"29296581","name":"Reboot","screen_name":"Just_Reboot","location":"NC","description":"G+ http:\/\/t.co\/MyXmhCAtnD\r\n#TeamKang PR, Attempted Writer, IT Tech, Social Media Berserker and Entertainer, Adjectives, Assertions, Disclaimers","url":null,"entities":{"description":{"urls":[{"url":"http:\/\/t.co\/MyXmhCAtnD","expanded_url":"http:\/\/goo.gl\/giVTc","display_url":"goo.gl\/giVTc","indices":[3,25]}]}},"protected":false,"followers_count":2244,"friends_count":435,"listed_count":60,"created_at":"Mon Apr 06 21:21:27 +0000 2009","favourites_count":79,"utc_offset":-18000,"time_zone":"Eastern Time (US & Canada)","geo_enabled":false,"verified":false,"statuses_count":27703,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"131516","profile_background_image_url":"http:\/\/a0.twimg.com\/profile_background_images\/871578268\/5db26751bf732750f5ca1ed4f9f59309.png","profile_background_image_url_https":"https:\/\/si0.twimg.com\/profile_background_images\/871578268\/5db26751bf732750f5ca1ed4f9f59309.png","profile_background_tile":true,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/378800000480531175\/93e8b5cb95a635ebb81302f5b6044a76_normal.jpeg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/378800000480531175\/93e8b5cb95a635ebb81302f5b6044a76_normal.jpeg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/29296581\/1368898490","profile_link_color":"009999","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"EFEFEF","profile_text_color":"333333","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":1,"favorite_count":1,"entities":{"hashtags":[{"text":"FreeBandNames","indices":[17,31]}],"symbols":[],"urls":[],"user_mentions":[]},"favorited":false,"retweeted":false,"lang":"en"},"retweet_count":1,"favorite_count":0,"entities":{"hashtags":[{"text":"FreeBandNames","indices":[34,48]}],"symbols":[],"urls":[],"user_mentions":[{"screen_name":"Just_Reboot","name":"Reboot","id":29296581,"id_str":"29296581","indices":[3,15]}]},"favorited":false,"retweeted":false,"lang":"en"},{"metadata":{"result_type":"recent","iso_language_code":"en"},"created_at":"Fri Dec 20 16:34:40 +0000 2013","id":414071361066532864,"id_str":"414071361066532864","text":"The Dick Tonsils #FreeBandNames\u00a0","source":"\u003ca href=\"http:\/\/www.myplume.com\/\" rel=\"nofollow\"\u003ePlume\u00a0for\u00a0Android\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":29296581,"id_str":"29296581","name":"Reboot","screen_name":"Just_Reboot","location":"NC","description":"G+ http:\/\/t.co\/MyXmhCAtnD\r\n#TeamKang PR, Attempted Writer, IT Tech, Social Media Berserker and Entertainer, Adjectives, Assertions, Disclaimers","url":null,"entities":{"description":{"urls":[{"url":"http:\/\/t.co\/MyXmhCAtnD","expanded_url":"http:\/\/goo.gl\/giVTc","display_url":"goo.gl\/giVTc","indices":[3,25]}]}},"protected":false,"followers_count":2244,"friends_count":435,"listed_count":60,"created_at":"Mon Apr 06 21:21:27 +0000 2009","favourites_count":79,"utc_offset":-18000,"time_zone":"Eastern Time (US & Canada)","geo_enabled":false,"verified":false,"statuses_count":27703,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"131516","profile_background_image_url":"http:\/\/a0.twimg.com\/profile_background_images\/871578268\/5db26751bf732750f5ca1ed4f9f59309.png","profile_background_image_url_https":"https:\/\/si0.twimg.com\/profile_background_images\/871578268\/5db26751bf732750f5ca1ed4f9f59309.png","profile_background_tile":true,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/378800000480531175\/93e8b5cb95a635ebb81302f5b6044a76_normal.jpeg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/378800000480531175\/93e8b5cb95a635ebb81302f5b6044a76_normal.jpeg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/29296581\/1368898490","profile_link_color":"009999","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"EFEFEF","profile_text_color":"333333","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":1,"favorite_count":1,"entities":{"hashtags":[{"text":"FreeBandNames","indices":[17,31]}],"symbols":[],"urls":[],"user_mentions":[]},"favorited":false,"retweeted":false,"lang":"en"}],"search_metadata":{"completed_in":0.012,"max_id":414075829182676992,"max_id_str":"414075829182676992","next_results":"?max_id=414071361066532863&q=%23freebandnames&count=3&include_entities=1","query":"%23freebandnames","refresh_url":"?since_id=414075829182676992&q=%23freebandnames&include_entities=1","count":3,"since_id":0,"since_id_str":"0"}} |
@@ -0,0 +1,17 @@ |
||
1 |
+<html> |
|
2 |
+ <head> |
|
3 |
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
|
4 |
+ <title>test</title> |
|
5 |
+ </head> |
|
6 |
+ <body> |
|
7 |
+ <ul> |
|
8 |
+ <li><a href="http://google.com">google</a></li> |
|
9 |
+ <li><a href="https://www.google.ca/search?q=some query">broken</a></li> |
|
10 |
+ <li><a href="https://www.google.ca/search?q=some%20query">escaped</a></li> |
|
11 |
+ <li><a href="http://ko.wikipedia.org/wiki/위키백과:대문">unicode url</a></li> |
|
12 |
+ <li><a href="https://www.google.ca/search?q=위키백과:대문">unicode param</a></li> |
|
13 |
+ <li><a href="http://ko.wikipedia.org/wiki/%EC%9C%84%ED%82%A4%EB%B0%B1%EA%B3%BC:%EB%8C%80%EB%AC%B8">percent encoded url</a></li> |
|
14 |
+ <li><a href="https://www.google.ca/search?q=%EC%9C%84%ED%82%A4%EB%B0%B1%EA%B3%BC:%EB%8C%80%EB%AC%B8">percent encoded param</a></li> |
|
15 |
+ </ul> |
|
16 |
+ </body> |
|
17 |
+</html> |
@@ -111,3 +111,24 @@ jane_basecamp_agent: |
||
111 | 111 |
user: jane |
112 | 112 |
service: generic |
113 | 113 |
guid: <%= SecureRandom.hex %> |
114 |
+ |
|
115 |
+ |
|
116 |
+bob_data_output_agent: |
|
117 |
+ type: Agents::DataOutputAgent |
|
118 |
+ user: bob |
|
119 |
+ name: RSS Feed |
|
120 |
+ guid: <%= SecureRandom.hex %> |
|
121 |
+ options: <%= { |
|
122 |
+ expected_receive_period_in_days: 3, |
|
123 |
+ secrets: ['secret'], |
|
124 |
+ template: { |
|
125 |
+ title: 'unchanged', |
|
126 |
+ description: 'unchanged', |
|
127 |
+ item: { |
|
128 |
+ title: 'unchanged', |
|
129 |
+ description: 'unchanged', |
|
130 |
+ author: 'unchanged', |
|
131 |
+ link: 'http://example.com' |
|
132 |
+ } |
|
133 |
+ } |
|
134 |
+ }.to_json.inspect %> |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe ApplicationHelper do |
4 | 4 |
describe '#icon_tag' do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe DotHelper do |
4 | 4 |
describe "with example Agents" do |
@@ -72,7 +72,7 @@ describe DotHelper do |
||
72 | 72 |
end |
73 | 73 |
|
74 | 74 |
it "generates a richer DOT script" do |
75 |
- expect(agents_dot(@agents, true)).to match(%r{ |
|
75 |
+ expect(agents_dot(@agents, rich: true)).to match(%r{ |
|
76 | 76 |
\A |
77 | 77 |
digraph \x20 "Agent \x20 Event \x20 Flow" \{ |
78 | 78 |
node \[ [^\]]+ \]; |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe JobsHelper do |
4 | 4 |
let(:job) { Delayed::Job.new } |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe MarkdownHelper do |
4 | 4 |
|
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe ScenarioHelper do |
4 | 4 |
let(:scenario) { users(:bob).scenarios.build(name: 'Scene', tag_fg_color: '#AAAAAA', tag_bg_color: '#000000') } |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe AgentRunner do |
4 | 4 |
context "without traps" do |
@@ -1,6 +1,6 @@ |
||
1 | 1 |
# encoding: utf-8 |
2 | 2 |
|
3 |
-require 'spec_helper' |
|
3 |
+require 'rails_helper' |
|
4 | 4 |
|
5 | 5 |
describe AgentsExporter do |
6 | 6 |
describe "#as_json" do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe DelayedJobWorker do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
require 'huginn_scheduler' |
3 | 3 |
|
4 | 4 |
describe HuginnScheduler do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe LiquidMigrator do |
4 | 4 |
describe "converting JSONPath strings" do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Location do |
4 | 4 |
let(:location) { |
@@ -30,14 +30,14 @@ describe Location do |
||
30 | 30 |
expect(location['lat']).to eq 2.0 |
31 | 31 |
end |
32 | 32 |
|
33 |
- it "has a convencience accessor for combined latitude and longitude" do |
|
33 |
+ it "has a convenience accessor for combined latitude and longitude" do |
|
34 | 34 |
expect(location.latlng).to eq "2.0,3.0" |
35 | 35 |
end |
36 | 36 |
|
37 | 37 |
it "does not allow hash-style assignment" do |
38 | 38 |
expect { |
39 | 39 |
location[:lat] = 2.0 |
40 |
- }.to raise_error |
|
40 |
+ }.to raise_error(NoMethodError) |
|
41 | 41 |
end |
42 | 42 |
|
43 | 43 |
it "ignores invalid values" do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Utils do |
4 | 4 |
describe "#unindent" do |
@@ -1,5 +1,5 @@ |
||
1 | 1 |
# -*- coding: utf-8 -*- |
2 |
-require 'spec_helper' |
|
2 |
+require 'rails_helper' |
|
3 | 3 |
|
4 | 4 |
describe AgentLog do |
5 | 5 |
describe "validations" do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agent do |
4 | 4 |
it_behaves_like WorkingHelpers |
@@ -223,7 +223,7 @@ describe Agent do |
||
223 | 223 |
mock(Agent).find(@checker.id) { @checker } |
224 | 224 |
expect { |
225 | 225 |
Agents::SomethingSource.async_check(@checker.id) |
226 |
- }.to raise_error |
|
226 |
+ }.to raise_error(RuntimeError) |
|
227 | 227 |
log = @checker.logs.first |
228 | 228 |
expect(log.message).to match(/Exception/) |
229 | 229 |
expect(log.level).to eq(4) |
@@ -263,7 +263,7 @@ describe Agent do |
||
263 | 263 |
Agent.async_check(agents(:bob_weather_agent).id) |
264 | 264 |
expect { |
265 | 265 |
Agent.async_receive(agents(:bob_rain_notifier_agent).id, [agents(:bob_weather_agent).events.last.id]) |
266 |
- }.to raise_error |
|
266 |
+ }.to raise_error(RuntimeError) |
|
267 | 267 |
log = agents(:bob_rain_notifier_agent).logs.first |
268 | 268 |
expect(log.message).to match(/Exception/) |
269 | 269 |
expect(log.level).to eq(4) |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::AdiosoAgent do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
require 'models/concerns/oauthable' |
3 | 3 |
|
4 | 4 |
describe Agents::BasecampAgent do |
@@ -0,0 +1,145 @@ |
||
1 |
+require 'rails_helper' |
|
2 |
+ |
|
3 |
+ |
|
4 |
+describe Agents::BeeperAgent do |
|
5 |
+ let(:base_params) { |
|
6 |
+ { |
|
7 |
+ 'type' => 'message', |
|
8 |
+ 'app_id' => 'some-app-id', |
|
9 |
+ 'api_key' => 'some-api-key', |
|
10 |
+ 'sender_id' => 'sender-id', |
|
11 |
+ 'phone' => '+111111111111', |
|
12 |
+ 'text' => 'Some text' |
|
13 |
+ } |
|
14 |
+ } |
|
15 |
+ |
|
16 |
+ subject { |
|
17 |
+ agent = described_class.new(name: 'beeper-agent', options: base_params) |
|
18 |
+ agent.user = users(:jane) |
|
19 |
+ agent.save! and return agent |
|
20 |
+ } |
|
21 |
+ |
|
22 |
+ context 'validation' do |
|
23 |
+ it 'valid' do |
|
24 |
+ expect(subject).to be_valid |
|
25 |
+ end |
|
26 |
+ |
|
27 |
+ [:type, :app_id, :api_key, :sender_id].each do |attr| |
|
28 |
+ it "invalid without #{attr}" do |
|
29 |
+ subject.options[attr] = nil |
|
30 |
+ expect(subject).not_to be_valid |
|
31 |
+ end |
|
32 |
+ end |
|
33 |
+ |
|
34 |
+ it 'invalid with group_id and phone' do |
|
35 |
+ subject.options['group_id'] ='some-group-id' |
|
36 |
+ expect(subject).not_to be_valid |
|
37 |
+ end |
|
38 |
+ |
|
39 |
+ context '#message' do |
|
40 |
+ it 'requires text' do |
|
41 |
+ subject.options[:text] = nil |
|
42 |
+ expect(subject).not_to be_valid |
|
43 |
+ end |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ context '#image' do |
|
47 |
+ before(:each) do |
|
48 |
+ subject.options[:type] = 'image' |
|
49 |
+ end |
|
50 |
+ |
|
51 |
+ it 'invalid without image' do |
|
52 |
+ expect(subject).not_to be_valid |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ it 'valid with image' do |
|
56 |
+ subject.options[:image] = 'some-url' |
|
57 |
+ expect(subject).to be_valid |
|
58 |
+ end |
|
59 |
+ end |
|
60 |
+ |
|
61 |
+ context '#event' do |
|
62 |
+ before(:each) do |
|
63 |
+ subject.options[:type] = 'event' |
|
64 |
+ end |
|
65 |
+ |
|
66 |
+ it 'invalid without start_time' do |
|
67 |
+ expect(subject).not_to be_valid |
|
68 |
+ end |
|
69 |
+ |
|
70 |
+ it 'valid with start_time' do |
|
71 |
+ subject.options[:start_time] = Time.now |
|
72 |
+ expect(subject).to be_valid |
|
73 |
+ end |
|
74 |
+ end |
|
75 |
+ |
|
76 |
+ context '#location' do |
|
77 |
+ before(:each) do |
|
78 |
+ subject.options[:type] = 'location' |
|
79 |
+ end |
|
80 |
+ |
|
81 |
+ it 'invalid without latitude and longitude' do |
|
82 |
+ expect(subject).not_to be_valid |
|
83 |
+ end |
|
84 |
+ |
|
85 |
+ it 'valid with latitude and longitude' do |
|
86 |
+ subject.options[:latitude] = 15.0 |
|
87 |
+ subject.options[:longitude] = 16.0 |
|
88 |
+ expect(subject).to be_valid |
|
89 |
+ end |
|
90 |
+ end |
|
91 |
+ |
|
92 |
+ context '#task' do |
|
93 |
+ before(:each) do |
|
94 |
+ subject.options[:type] = 'task' |
|
95 |
+ end |
|
96 |
+ |
|
97 |
+ it 'valid with text' do |
|
98 |
+ expect(subject).to be_valid |
|
99 |
+ end |
|
100 |
+ end |
|
101 |
+ end |
|
102 |
+ |
|
103 |
+ context 'payload_for' do |
|
104 |
+ it 'removes unwanted attributes' do |
|
105 |
+ result = subject.send(:payload_for, {'type' => 'message', 'text' => 'text', |
|
106 |
+ 'sender_id' => 'sender', 'phone' => '+1', 'random_attribute' => 'unwanted'}) |
|
107 |
+ expect(result).to eq('{"text":"text","sender_id":"sender","phone":"+1"}') |
|
108 |
+ end |
|
109 |
+ end |
|
110 |
+ |
|
111 |
+ context 'headers' do |
|
112 |
+ it 'sets X-Beeper-Application-Id header with app_id' do |
|
113 |
+ expect(subject.send(:headers)['X-Beeper-Application-Id']).to eq(base_params['app_id']) |
|
114 |
+ end |
|
115 |
+ |
|
116 |
+ it 'sets X-Beeper-REST-API-Key header with api_key' do |
|
117 |
+ expect(subject.send(:headers)['X-Beeper-REST-API-Key']).to eq(base_params['api_key']) |
|
118 |
+ end |
|
119 |
+ |
|
120 |
+ it 'sets Content-Type' do |
|
121 |
+ expect(subject.send(:headers)['Content-Type']).to eq('application/json') |
|
122 |
+ end |
|
123 |
+ end |
|
124 |
+ |
|
125 |
+ context 'endpoint_for' do |
|
126 |
+ it 'returns valid URL for message' do |
|
127 |
+ expect(subject.send(:endpoint_for, 'message')).to eq('https://api.beeper.io/api/messages.json') |
|
128 |
+ end |
|
129 |
+ |
|
130 |
+ it 'returns valid URL for image' do |
|
131 |
+ expect(subject.send(:endpoint_for, 'image')).to eq('https://api.beeper.io/api/images.json') |
|
132 |
+ end |
|
133 |
+ |
|
134 |
+ it 'returns valid URL for event' do |
|
135 |
+ expect(subject.send(:endpoint_for, 'event')).to eq('https://api.beeper.io/api/events.json') |
|
136 |
+ end |
|
137 |
+ |
|
138 |
+ it 'returns valid URL for location' do |
|
139 |
+ expect(subject.send(:endpoint_for, 'location')).to eq('https://api.beeper.io/api/locations.json') |
|
140 |
+ end |
|
141 |
+ it 'returns valid URL for task' do |
|
142 |
+ expect(subject.send(:endpoint_for, 'task')).to eq('https://api.beeper.io/api/tasks.json') |
|
143 |
+ end |
|
144 |
+ end |
|
145 |
+end |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::ChangeDetectorAgent do |
4 | 4 |
def create_event(output=nil) |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::CommanderAgent do |
4 | 4 |
let(:valid_params) { |
@@ -19,10 +19,10 @@ describe Agents::CommanderAgent do |
||
19 | 19 |
|
20 | 20 |
it_behaves_like AgentControllerConcern |
21 | 21 |
|
22 |
- describe "check!" do |
|
22 |
+ describe "check" do |
|
23 | 23 |
it "should command targets" do |
24 | 24 |
stub(agent).control!.once { nil } |
25 |
- agent.check! |
|
25 |
+ agent.check |
|
26 | 26 |
end |
27 | 27 |
end |
28 | 28 |
|
@@ -1,6 +1,6 @@ |
||
1 | 1 |
# encoding: utf-8 |
2 | 2 |
|
3 |
-require 'spec_helper' |
|
3 |
+require 'rails_helper' |
|
4 | 4 |
|
5 | 5 |
describe Agents::DataOutputAgent do |
6 | 6 |
let(:agent) do |
@@ -73,6 +73,29 @@ describe Agents::DataOutputAgent do |
||
73 | 73 |
end |
74 | 74 |
end |
75 | 75 |
|
76 |
+ describe "#receive" do |
|
77 |
+ it "should push to hubs when push_hubs is given" do |
|
78 |
+ agent.options[:push_hubs] = %w[http://push.example.com] |
|
79 |
+ agent.options[:template] = { 'link' => 'http://huginn.example.org' } |
|
80 |
+ |
|
81 |
+ alist = nil |
|
82 |
+ |
|
83 |
+ stub_request(:post, 'http://push.example.com/') |
|
84 |
+ .with(headers: { 'Content-Type' => %r{\Aapplication/x-www-form-urlencoded\s*(?:;|\z)} }) |
|
85 |
+ .to_return { |request| |
|
86 |
+ alist = URI.decode_www_form(request.body).sort |
|
87 |
+ { status: 200, body: 'ok' } |
|
88 |
+ } |
|
89 |
+ |
|
90 |
+ agent.receive(events(:bob_website_agent_event)) |
|
91 |
+ |
|
92 |
+ expect(alist).to eq [ |
|
93 |
+ ["hub.mode", "publish"], |
|
94 |
+ ["hub.url", agent.feed_url(secret: agent.options[:secrets].first, format: :xml)] |
|
95 |
+ ] |
|
96 |
+ end |
|
97 |
+ end |
|
98 |
+ |
|
76 | 99 |
describe "#receive_web_request" do |
77 | 100 |
before do |
78 | 101 |
current_time = Time.now |
@@ -130,7 +153,7 @@ describe Agents::DataOutputAgent do |
||
130 | 153 |
expect(content_type).to eq('text/xml') |
131 | 154 |
expect(content.gsub(/\s+/, '')).to eq Utils.unindent(<<-XML).gsub(/\s+/, '') |
132 | 155 |
<?xml version="1.0" encoding="UTF-8" ?> |
133 |
- <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> |
|
156 |
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"> |
|
134 | 157 |
<channel> |
135 | 158 |
<atom:link href="https://yoursite.com/users/#{agent.user.id}/web_requests/#{agent.id}/secret1.xml" rel="self" type="application/rss+xml"/> |
136 | 159 |
<atom:icon>https://yoursite.com/favicon.ico</atom:icon> |
@@ -170,6 +193,16 @@ describe Agents::DataOutputAgent do |
||
170 | 193 |
XML |
171 | 194 |
end |
172 | 195 |
|
196 |
+ it "can output RSS with hub links when push_hubs is specified" do |
|
197 |
+ stub(agent).feed_link { "https://yoursite.com" } |
|
198 |
+ agent.options[:push_hubs] = %w[https://pubsubhubbub.superfeedr.com/ https://pubsubhubbub.appspot.com/] |
|
199 |
+ content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml') |
|
200 |
+ expect(status).to eq(200) |
|
201 |
+ expect(content_type).to eq('text/xml') |
|
202 |
+ xml = Nokogiri::XML(content) |
|
203 |
+ expect(xml.xpath('/rss/channel/atom:link[@rel="hub"]/@href').map(&:text).sort).to eq agent.options[:push_hubs].sort |
|
204 |
+ end |
|
205 |
+ |
|
173 | 206 |
it "can output JSON" do |
174 | 207 |
agent.options['template']['item']['foo'] = "hi" |
175 | 208 |
|
@@ -359,7 +392,7 @@ describe Agents::DataOutputAgent do |
||
359 | 392 |
expect(content_type).to eq('text/xml') |
360 | 393 |
expect(content.gsub(/\s+/, '')).to eq Utils.unindent(<<-XML).gsub(/\s+/, '') |
361 | 394 |
<?xml version="1.0" encoding="UTF-8" ?> |
362 |
- <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> |
|
395 |
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"> |
|
363 | 396 |
<channel> |
364 | 397 |
<atom:link href="https://yoursite.com/users/#{agent.user.id}/web_requests/#{agent.id}/secret1.xml" rel="self" type="application/rss+xml"/> |
365 | 398 |
<atom:icon>https://yoursite.com/favicon.ico</atom:icon> |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::DeDuplicationAgent do |
4 | 4 |
def create_event(output=nil) |
@@ -0,0 +1,127 @@ |
||
1 |
+require 'rails_helper' |
|
2 |
+ |
|
3 |
+describe Agents::DelayAgent do |
|
4 |
+ let(:agent) do |
|
5 |
+ _agent = Agents::DelayAgent.new(name: 'My DelayAgent') |
|
6 |
+ _agent.options = _agent.default_options.merge('max_events' => 2) |
|
7 |
+ _agent.user = users(:bob) |
|
8 |
+ _agent.sources << agents(:bob_website_agent) |
|
9 |
+ _agent.save! |
|
10 |
+ _agent |
|
11 |
+ end |
|
12 |
+ |
|
13 |
+ def create_event |
|
14 |
+ _event = Event.new(payload: { random: rand }) |
|
15 |
+ _event.agent = agents(:bob_website_agent) |
|
16 |
+ _event.save! |
|
17 |
+ _event |
|
18 |
+ end |
|
19 |
+ |
|
20 |
+ let(:first_event) { create_event } |
|
21 |
+ let(:second_event) { create_event } |
|
22 |
+ let(:third_event) { create_event } |
|
23 |
+ |
|
24 |
+ describe "#working?" do |
|
25 |
+ it "checks if events have been received within expected receive period" do |
|
26 |
+ expect(agent).not_to be_working |
|
27 |
+ Agents::DelayAgent.async_receive agent.id, [events(:bob_website_agent_event).id] |
|
28 |
+ expect(agent.reload).to be_working |
|
29 |
+ the_future = (agent.options[:expected_receive_period_in_days].to_i + 1).days.from_now |
|
30 |
+ stub(Time).now { the_future } |
|
31 |
+ expect(agent.reload).not_to be_working |
|
32 |
+ end |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ describe "validation" do |
|
36 |
+ before do |
|
37 |
+ expect(agent).to be_valid |
|
38 |
+ end |
|
39 |
+ |
|
40 |
+ it "should validate max_events" do |
|
41 |
+ agent.options.delete('max_events') |
|
42 |
+ expect(agent).not_to be_valid |
|
43 |
+ agent.options['max_events'] = "" |
|
44 |
+ expect(agent).not_to be_valid |
|
45 |
+ agent.options['max_events'] = "0" |
|
46 |
+ expect(agent).not_to be_valid |
|
47 |
+ agent.options['max_events'] = "10" |
|
48 |
+ expect(agent).to be_valid |
|
49 |
+ end |
|
50 |
+ |
|
51 |
+ it "should validate presence of expected_receive_period_in_days" do |
|
52 |
+ agent.options['expected_receive_period_in_days'] = "" |
|
53 |
+ expect(agent).not_to be_valid |
|
54 |
+ agent.options['expected_receive_period_in_days'] = 0 |
|
55 |
+ expect(agent).not_to be_valid |
|
56 |
+ agent.options['expected_receive_period_in_days'] = -1 |
|
57 |
+ expect(agent).not_to be_valid |
|
58 |
+ end |
|
59 |
+ |
|
60 |
+ it "should validate keep" do |
|
61 |
+ agent.options.delete('keep') |
|
62 |
+ expect(agent).not_to be_valid |
|
63 |
+ agent.options['keep'] = "" |
|
64 |
+ expect(agent).not_to be_valid |
|
65 |
+ agent.options['keep'] = 'wrong' |
|
66 |
+ expect(agent).not_to be_valid |
|
67 |
+ agent.options['keep'] = 'newest' |
|
68 |
+ expect(agent).to be_valid |
|
69 |
+ agent.options['keep'] = 'oldest' |
|
70 |
+ expect(agent).to be_valid |
|
71 |
+ end |
|
72 |
+ end |
|
73 |
+ |
|
74 |
+ describe "#receive" do |
|
75 |
+ it "records Events" do |
|
76 |
+ expect(agent.memory).to be_empty |
|
77 |
+ agent.receive([first_event]) |
|
78 |
+ expect(agent.memory).not_to be_empty |
|
79 |
+ agent.receive([second_event]) |
|
80 |
+ expect(agent.memory['event_ids']).to eq [first_event.id, second_event.id] |
|
81 |
+ end |
|
82 |
+ |
|
83 |
+ it "keeps the newest when 'keep' is set to 'newest'" do |
|
84 |
+ expect(agent.options['keep']).to eq 'newest' |
|
85 |
+ agent.receive([first_event, second_event, third_event]) |
|
86 |
+ expect(agent.memory['event_ids']).to eq [second_event.id, third_event.id] |
|
87 |
+ end |
|
88 |
+ |
|
89 |
+ it "keeps the oldest when 'keep' is set to 'oldest'" do |
|
90 |
+ agent.options['keep'] = 'oldest' |
|
91 |
+ agent.receive([first_event, second_event, third_event]) |
|
92 |
+ expect(agent.memory['event_ids']).to eq [first_event.id, second_event.id] |
|
93 |
+ end |
|
94 |
+ end |
|
95 |
+ |
|
96 |
+ describe "#check" do |
|
97 |
+ it "re-emits Events and clears the memory" do |
|
98 |
+ agent.receive([first_event, second_event, third_event]) |
|
99 |
+ expect(agent.memory['event_ids']).to eq [second_event.id, third_event.id] |
|
100 |
+ |
|
101 |
+ expect { |
|
102 |
+ agent.check |
|
103 |
+ }.to change { agent.events.count }.by(2) |
|
104 |
+ |
|
105 |
+ events = agent.events.reorder('events.id desc') |
|
106 |
+ expect(events.first.payload).to eq third_event.payload |
|
107 |
+ expect(events.second.payload).to eq second_event.payload |
|
108 |
+ |
|
109 |
+ expect(agent.memory['event_ids']).to eq [] |
|
110 |
+ end |
|
111 |
+ |
|
112 |
+ it "re-emits max_emitted_events and clears just them from the memory" do |
|
113 |
+ agent.options['max_emitted_events'] = 1 |
|
114 |
+ agent.receive([first_event, second_event, third_event]) |
|
115 |
+ expect(agent.memory['event_ids']).to eq [second_event.id, third_event.id] |
|
116 |
+ |
|
117 |
+ expect { |
|
118 |
+ agent.check |
|
119 |
+ }.to change { agent.events.count }.by(1) |
|
120 |
+ |
|
121 |
+ events = agent.events.reorder('events.id desc') |
|
122 |
+ expect(agent.memory['event_ids']).to eq [third_event.id] |
|
123 |
+ expect(events.first.payload).to eq second_event.payload |
|
124 |
+ |
|
125 |
+ end |
|
126 |
+ end |
|
127 |
+end |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::DropboxFileUrlAgent do |
4 | 4 |
before(:each) do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::DropboxWatchAgent do |
4 | 4 |
before(:each) do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::EmailAgent do |
4 | 4 |
it_behaves_like EmailConcern |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::EmailDigestAgent do |
4 | 4 |
it_behaves_like EmailConcern |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::EventFormattingAgent do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::EvernoteAgent do |
4 | 4 |
class FakeEvernoteNoteStore |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
require 'time' |
3 | 3 |
|
4 | 4 |
describe Agents::FtpsiteAgent do |
@@ -26,7 +26,7 @@ describe Agents::FtpsiteAgent do |
||
26 | 26 |
|
27 | 27 |
it "should validate the integer fields" do |
28 | 28 |
@checker.options['expected_update_period_in_days'] = "nonsense" |
29 |
- expect { @checker.save! }.to raise_error; |
|
29 |
+ expect { @checker.save! }.to raise_error(/Invalid expected_update_period_in_days format/); |
|
30 | 30 |
@checker.options = @site |
31 | 31 |
end |
32 | 32 |
|
@@ -0,0 +1,112 @@ |
||
1 |
+require 'rails_helper' |
|
2 |
+ |
|
3 |
+describe Agents::GapDetectorAgent do |
|
4 |
+ let(:valid_params) { |
|
5 |
+ { |
|
6 |
+ 'name' => "my gap detector agent", |
|
7 |
+ 'options' => { |
|
8 |
+ 'window_duration_in_days' => "2", |
|
9 |
+ 'message' => "A gap was found!" |
|
10 |
+ } |
|
11 |
+ } |
|
12 |
+ } |
|
13 |
+ |
|
14 |
+ let(:agent) { |
|
15 |
+ _agent = Agents::GapDetectorAgent.new(valid_params) |
|
16 |
+ _agent.user = users(:bob) |
|
17 |
+ _agent.save! |
|
18 |
+ _agent |
|
19 |
+ } |
|
20 |
+ |
|
21 |
+ describe 'validation' do |
|
22 |
+ before do |
|
23 |
+ expect(agent).to be_valid |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ it 'should validate presence of message' do |
|
27 |
+ agent.options['message'] = nil |
|
28 |
+ expect(agent).not_to be_valid |
|
29 |
+ end |
|
30 |
+ |
|
31 |
+ it 'should validate presence of window_duration_in_days' do |
|
32 |
+ agent.options['window_duration_in_days'] = "" |
|
33 |
+ expect(agent).not_to be_valid |
|
34 |
+ |
|
35 |
+ agent.options['window_duration_in_days'] = "wrong" |
|
36 |
+ expect(agent).not_to be_valid |
|
37 |
+ |
|
38 |
+ agent.options['window_duration_in_days'] = "1" |
|
39 |
+ expect(agent).to be_valid |
|
40 |
+ |
|
41 |
+ agent.options['window_duration_in_days'] = "0.5" |
|
42 |
+ expect(agent).to be_valid |
|
43 |
+ end |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ describe '#receive' do |
|
47 |
+ it 'records the event if it has a created_at newer than the last seen' do |
|
48 |
+ agent.receive([events(:bob_website_agent_event)]) |
|
49 |
+ expect(agent.memory['newest_event_created_at']).to eq events(:bob_website_agent_event).created_at.to_i |
|
50 |
+ |
|
51 |
+ events(:bob_website_agent_event).created_at = 2.days.ago |
|
52 |
+ |
|
53 |
+ expect { |
|
54 |
+ agent.receive([events(:bob_website_agent_event)]) |
|
55 |
+ }.to_not change { agent.memory['newest_event_created_at'] } |
|
56 |
+ |
|
57 |
+ events(:bob_website_agent_event).created_at = 2.days.from_now |
|
58 |
+ |
|
59 |
+ expect { |
|
60 |
+ agent.receive([events(:bob_website_agent_event)]) |
|
61 |
+ }.to change { agent.memory['newest_event_created_at'] }.to(events(:bob_website_agent_event).created_at.to_i) |
|
62 |
+ end |
|
63 |
+ |
|
64 |
+ it 'ignores the event if value_path is present and the value at the path is blank' do |
|
65 |
+ agent.options['value_path'] = 'title' |
|
66 |
+ agent.receive([events(:bob_website_agent_event)]) |
|
67 |
+ expect(agent.memory['newest_event_created_at']).to eq events(:bob_website_agent_event).created_at.to_i |
|
68 |
+ |
|
69 |
+ events(:bob_website_agent_event).created_at = 2.days.from_now |
|
70 |
+ events(:bob_website_agent_event).payload['title'] = '' |
|
71 |
+ |
|
72 |
+ expect { |
|
73 |
+ agent.receive([events(:bob_website_agent_event)]) |
|
74 |
+ }.to_not change { agent.memory['newest_event_created_at'] } |
|
75 |
+ |
|
76 |
+ events(:bob_website_agent_event).payload['title'] = 'present!' |
|
77 |
+ |
|
78 |
+ expect { |
|
79 |
+ agent.receive([events(:bob_website_agent_event)]) |
|
80 |
+ }.to change { agent.memory['newest_event_created_at'] }.to(events(:bob_website_agent_event).created_at.to_i) |
|
81 |
+ end |
|
82 |
+ |
|
83 |
+ it 'clears any previous alert' do |
|
84 |
+ agent.memory['alerted_at'] = 2.days.ago.to_i |
|
85 |
+ agent.receive([events(:bob_website_agent_event)]) |
|
86 |
+ expect(agent.memory).to_not have_key('alerted_at') |
|
87 |
+ end |
|
88 |
+ end |
|
89 |
+ |
|
90 |
+ describe '#check' do |
|
91 |
+ it 'alerts once if no data has been received during window_duration_in_days' do |
|
92 |
+ agent.memory['newest_event_created_at'] = 1.days.ago.to_i |
|
93 |
+ |
|
94 |
+ expect { |
|
95 |
+ agent.check |
|
96 |
+ }.to_not change { agent.events.count } |
|
97 |
+ |
|
98 |
+ agent.memory['newest_event_created_at'] = 3.days.ago.to_i |
|
99 |
+ |
|
100 |
+ expect { |
|
101 |
+ agent.check |
|
102 |
+ }.to change { agent.events.count }.by(1) |
|
103 |
+ |
|
104 |
+ expect(agent.events.last.payload).to eq ({ 'message' => 'A gap was found!', |
|
105 |
+ 'gap_started_at' => agent.memory['newest_event_created_at'] }) |
|
106 |
+ |
|
107 |
+ expect { |
|
108 |
+ agent.check |
|
109 |
+ }.not_to change { agent.events.count } |
|
110 |
+ end |
|
111 |
+ end |
|
112 |
+end |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::GoogleCalendarPublishAgent, :vcr do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::GrowlAgent do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::HipchatAgent do |
4 | 4 |
before(:each) do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::HumanTaskAgent do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
require 'time' |
3 | 3 |
|
4 | 4 |
describe Agents::ImapFolderAgent do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::JabberAgent do |
4 | 4 |
let(:sent) { [] } |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::JavaScriptAgent do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::JiraAgent do |
4 | 4 |
before(:each) do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
require 'mqtt' |
3 | 3 |
require './spec/support/fake_mqtt_server' |
4 | 4 |
|
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::PdfInfoAgent do |
4 | 4 |
let(:agent) do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::PeakDetectorAgent do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
require 'ostruct' |
3 | 3 |
|
4 | 4 |
describe Agents::PostAgent do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
describe Agents::PublicTransportAgent do |
3 | 3 |
before do |
4 | 4 |
valid_params = { |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::PushbulletAgent do |
4 | 4 |
before(:each) do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::PushoverAgent do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::RssAgent do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::SchedulerAgent do |
4 | 4 |
let(:valid_params) { |
@@ -84,11 +84,4 @@ describe Agents::SchedulerAgent do |
||
84 | 84 |
expect(agent.memory['scheduled_at']).to be_nil |
85 | 85 |
end |
86 | 86 |
end |
87 |
- |
|
88 |
- describe "check!" do |
|
89 |
- it "should control targets" do |
|
90 |
- stub(agent).control!.once { nil } |
|
91 |
- agent.check! |
|
92 |
- end |
|
93 |
- end |
|
94 | 87 |
end |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::SentimentAgent do |
4 | 4 |
before do |
@@ -1,23 +1,35 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::ShellCommandAgent do |
4 | 4 |
before do |
5 | 5 |
@valid_path = Dir.pwd |
6 | 6 |
|
7 | 7 |
@valid_params = { |
8 |
- :path => @valid_path, |
|
9 |
- :command => "pwd", |
|
10 |
- :expected_update_period_in_days => "1", |
|
11 |
- } |
|
8 |
+ path: @valid_path, |
|
9 |
+ command: 'pwd', |
|
10 |
+ expected_update_period_in_days: '1', |
|
11 |
+ } |
|
12 |
+ |
|
13 |
+ @valid_params2 = { |
|
14 |
+ path: @valid_path, |
|
15 |
+ command: [RbConfig.ruby, '-e', 'puts "hello, #{STDIN.eof? ? "world" : STDIN.read.strip}."; STDERR.puts "warning!"'], |
|
16 |
+ stdin: "{{name}}", |
|
17 |
+ expected_update_period_in_days: '1', |
|
18 |
+ } |
|
12 | 19 |
|
13 |
- @checker = Agents::ShellCommandAgent.new(:name => "somename", :options => @valid_params) |
|
20 |
+ @checker = Agents::ShellCommandAgent.new(name: 'somename', options: @valid_params) |
|
14 | 21 |
@checker.user = users(:jane) |
15 | 22 |
@checker.save! |
16 | 23 |
|
24 |
+ @checker2 = Agents::ShellCommandAgent.new(name: 'somename2', options: @valid_params2) |
|
25 |
+ @checker2.user = users(:jane) |
|
26 |
+ @checker2.save! |
|
27 |
+ |
|
17 | 28 |
@event = Event.new |
18 | 29 |
@event.agent = agents(:jane_weather_agent) |
19 | 30 |
@event.payload = { |
20 |
- :cmd => "ls" |
|
31 |
+ 'name' => 'Huginn', |
|
32 |
+ 'cmd' => 'ls', |
|
21 | 33 |
} |
22 | 34 |
@event.save! |
23 | 35 |
|
@@ -27,6 +39,7 @@ describe Agents::ShellCommandAgent do |
||
27 | 39 |
describe "validation" do |
28 | 40 |
before do |
29 | 41 |
expect(@checker).to be_valid |
42 |
+ expect(@checker2).to be_valid |
|
30 | 43 |
end |
31 | 44 |
|
32 | 45 |
it "should validate presence of necessary fields" do |
@@ -47,7 +60,7 @@ describe Agents::ShellCommandAgent do |
||
47 | 60 |
|
48 | 61 |
describe "#working?" do |
49 | 62 |
it "generating events as scheduled" do |
50 |
- stub(@checker).run_command(@valid_path, 'pwd') { ["fake pwd output", "", 0] } |
|
63 |
+ stub(@checker).run_command(@valid_path, 'pwd', nil) { ["fake pwd output", "", 0] } |
|
51 | 64 |
|
52 | 65 |
expect(@checker).not_to be_working |
53 | 66 |
@checker.check |
@@ -60,7 +73,9 @@ describe Agents::ShellCommandAgent do |
||
60 | 73 |
|
61 | 74 |
describe "#check" do |
62 | 75 |
before do |
63 |
- stub(@checker).run_command(@valid_path, 'pwd') { ["fake pwd output", "", 0] } |
|
76 |
+ stub(@checker).run_command(@valid_path, 'pwd', nil) { ["fake pwd output", "", 0] } |
|
77 |
+ stub(@checker).run_command(@valid_path, 'empty_output', nil) { ["", "", 0] } |
|
78 |
+ stub(@checker).run_command(@valid_path, 'failure', nil) { ["failed", "error message", 1] } |
|
64 | 79 |
end |
65 | 80 |
|
66 | 81 |
it "should create an event when checking" do |
@@ -70,6 +85,42 @@ describe Agents::ShellCommandAgent do |
||
70 | 85 |
expect(Event.last.payload[:output]).to eq("fake pwd output") |
71 | 86 |
end |
72 | 87 |
|
88 |
+ it "should create an event when checking (unstubbed)" do |
|
89 |
+ expect { @checker2.check }.to change { Event.count }.by(1) |
|
90 |
+ expect(Event.last.payload[:path]).to eq(@valid_path) |
|
91 |
+ expect(Event.last.payload[:command]).to eq([RbConfig.ruby, '-e', 'puts "hello, #{STDIN.eof? ? "world" : STDIN.read.strip}."; STDERR.puts "warning!"']) |
|
92 |
+ expect(Event.last.payload[:output]).to eq('hello, world.') |
|
93 |
+ expect(Event.last.payload[:errors]).to eq('warning!') |
|
94 |
+ end |
|
95 |
+ |
|
96 |
+ describe "with suppress_on_empty_output" do |
|
97 |
+ it "should suppress events on empty output" do |
|
98 |
+ @checker.options[:suppress_on_empty_output] = true |
|
99 |
+ @checker.options[:command] = 'empty_output' |
|
100 |
+ expect { @checker.check }.not_to change { Event.count } |
|
101 |
+ end |
|
102 |
+ |
|
103 |
+ it "should not suppress events on non-empty output" do |
|
104 |
+ @checker.options[:suppress_on_empty_output] = true |
|
105 |
+ @checker.options[:command] = 'failure' |
|
106 |
+ expect { @checker.check }.to change { Event.count }.by(1) |
|
107 |
+ end |
|
108 |
+ end |
|
109 |
+ |
|
110 |
+ describe "with suppress_on_failure" do |
|
111 |
+ it "should suppress events on failure" do |
|
112 |
+ @checker.options[:suppress_on_failure] = true |
|
113 |
+ @checker.options[:command] = 'failure' |
|
114 |
+ expect { @checker.check }.not_to change { Event.count } |
|
115 |
+ end |
|
116 |
+ |
|
117 |
+ it "should not suppress events on success" do |
|
118 |
+ @checker.options[:suppress_on_failure] = true |
|
119 |
+ @checker.options[:command] = 'empty_output' |
|
120 |
+ expect { @checker.check }.to change { Event.count }.by(1) |
|
121 |
+ end |
|
122 |
+ end |
|
123 |
+ |
|
73 | 124 |
it "does not run when should_run? is false" do |
74 | 125 |
stub(Agents::ShellCommandAgent).should_run? { false } |
75 | 126 |
expect { @checker.check }.not_to change { Event.count } |
@@ -78,7 +129,7 @@ describe Agents::ShellCommandAgent do |
||
78 | 129 |
|
79 | 130 |
describe "#receive" do |
80 | 131 |
before do |
81 |
- stub(@checker).run_command(@valid_path, @event.payload[:cmd]) { ["fake ls output", "", 0] } |
|
132 |
+ stub(@checker).run_command(@valid_path, @event.payload[:cmd], nil) { ["fake ls output", "", 0] } |
|
82 | 133 |
end |
83 | 134 |
|
84 | 135 |
it "creates events" do |
@@ -89,6 +140,13 @@ describe Agents::ShellCommandAgent do |
||
89 | 140 |
expect(Event.last.payload[:output]).to eq("fake ls output") |
90 | 141 |
end |
91 | 142 |
|
143 |
+ it "creates events (unstubbed)" do |
|
144 |
+ @checker2.receive([@event]) |
|
145 |
+ expect(Event.last.payload[:path]).to eq(@valid_path) |
|
146 |
+ expect(Event.last.payload[:output]).to eq('hello, Huginn.') |
|
147 |
+ expect(Event.last.payload[:errors]).to eq('warning!') |
|
148 |
+ end |
|
149 |
+ |
|
92 | 150 |
it "does not run when should_run? is false" do |
93 | 151 |
stub(Agents::ShellCommandAgent).should_run? { false } |
94 | 152 |
|
@@ -1,12 +1,15 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::SlackAgent do |
4 | 4 |
before(:each) do |
5 |
+ @fallback = "Its going to rain" |
|
6 |
+ @attachments = [{'fallback' => "{{fallback}}"}] |
|
5 | 7 |
@valid_params = { |
6 | 8 |
'webhook_url' => 'https://hooks.slack.com/services/random1/random2/token', |
7 | 9 |
'channel' => '#random', |
8 | 10 |
'username' => "{{username}}", |
9 |
- 'message' => "{{message}}" |
|
11 |
+ 'message' => "{{message}}", |
|
12 |
+ 'attachments' => @attachments |
|
10 | 13 |
} |
11 | 14 |
|
12 | 15 |
@checker = Agents::SlackAgent.new(:name => "slacker", :options => @valid_params) |
@@ -15,7 +18,7 @@ describe Agents::SlackAgent do |
||
15 | 18 |
|
16 | 19 |
@event = Event.new |
17 | 20 |
@event.agent = agents(:bob_weather_agent) |
18 |
- @event.payload = { :channel => '#random', :message => 'Looks like its going to rain', username: "Huggin user"} |
|
21 |
+ @event.payload = { :channel => '#random', :message => 'Looks like its going to rain', username: "Huggin user", fallback: @fallback} |
|
19 | 22 |
@event.save! |
20 | 23 |
end |
21 | 24 |
|
@@ -44,12 +47,22 @@ describe Agents::SlackAgent do |
||
44 | 47 |
@checker.options['icon_emoji'] = "something" |
45 | 48 |
expect(@checker).to be_valid |
46 | 49 |
end |
50 |
+ |
|
51 |
+ it "should allow attachments" do |
|
52 |
+ @checker.options['attachments'] = nil |
|
53 |
+ expect(@checker).to be_valid |
|
54 |
+ @checker.options['attachments'] = [] |
|
55 |
+ expect(@checker).to be_valid |
|
56 |
+ @checker.options['attachments'] = @attachments |
|
57 |
+ expect(@checker).to be_valid |
|
58 |
+ end |
|
47 | 59 |
end |
48 | 60 |
|
49 | 61 |
describe "#receive" do |
50 | 62 |
it "receive an event without errors" do |
51 | 63 |
any_instance_of(Slack::Notifier) do |obj| |
52 | 64 |
mock(obj).ping(@event.payload[:message], |
65 |
+ attachments: [{'fallback' => @fallback}], |
|
53 | 66 |
channel: @event.payload[:channel], |
54 | 67 |
username: @event.payload[:username] |
55 | 68 |
) |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::StubhubAgent do |
4 | 4 |
|
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::TranslationAgent do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::TriggerAgent do |
4 | 4 |
before do |
@@ -58,6 +58,27 @@ describe Agents::TriggerAgent do |
||
58 | 58 |
expect(@checker).not_to be_valid |
59 | 59 |
end |
60 | 60 |
|
61 |
+ it "validates that 'must_match' is a positive integer, not greater than the number of rules, if provided" do |
|
62 |
+ @checker.options['must_match'] = '1' |
|
63 |
+ expect(@checker).to be_valid |
|
64 |
+ |
|
65 |
+ @checker.options['must_match'] = '0' |
|
66 |
+ expect(@checker).not_to be_valid |
|
67 |
+ |
|
68 |
+ @checker.options['must_match'] = 'wrong' |
|
69 |
+ expect(@checker).not_to be_valid |
|
70 |
+ |
|
71 |
+ @checker.options['must_match'] = '' |
|
72 |
+ expect(@checker).to be_valid |
|
73 |
+ |
|
74 |
+ @checker.options.delete('must_match') |
|
75 |
+ expect(@checker).to be_valid |
|
76 |
+ |
|
77 |
+ @checker.options['must_match'] = '2' |
|
78 |
+ expect(@checker).not_to be_valid |
|
79 |
+ expect(@checker.errors[:base].first).to match(/equal to or less than the number of rules/) |
|
80 |
+ end |
|
81 |
+ |
|
61 | 82 |
it "should validate the three fields in each rule" do |
62 | 83 |
@checker.options['rules'] << { 'path' => "foo", 'type' => "fake", 'value' => "6" } |
63 | 84 |
expect(@checker).not_to be_valid |
@@ -283,23 +304,59 @@ describe Agents::TriggerAgent do |
||
283 | 304 |
}.to change { Event.count }.by(2) |
284 | 305 |
end |
285 | 306 |
|
286 |
- it "handles ANDing rules together" do |
|
287 |
- @checker.options['rules'] << { |
|
288 |
- 'type' => "field>=value", |
|
289 |
- 'value' => "4", |
|
290 |
- 'path' => "foo.bing" |
|
291 |
- } |
|
307 |
+ describe "with multiple rules" do |
|
308 |
+ before do |
|
309 |
+ @checker.options['rules'] << { |
|
310 |
+ 'type' => "field>=value", |
|
311 |
+ 'value' => "4", |
|
312 |
+ 'path' => "foo.bing" |
|
313 |
+ } |
|
314 |
+ end |
|
292 | 315 |
|
293 |
- @event.payload['foo']["bing"] = "5" |
|
316 |
+ it "handles ANDing rules together" do |
|
317 |
+ @event.payload['foo']["bing"] = "5" |
|
294 | 318 |
|
295 |
- expect { |
|
296 |
- @checker.receive([@event]) |
|
297 |
- }.to change { Event.count }.by(1) |
|
319 |
+ expect { |
|
320 |
+ @checker.receive([@event]) |
|
321 |
+ }.to change { Event.count }.by(1) |
|
298 | 322 |
|
299 |
- @checker.options['rules'].last['value'] = 6 |
|
300 |
- expect { |
|
301 |
- @checker.receive([@event]) |
|
302 |
- }.not_to change { Event.count } |
|
323 |
+ @event.payload['foo']["bing"] = "2" |
|
324 |
+ |
|
325 |
+ expect { |
|
326 |
+ @checker.receive([@event]) |
|
327 |
+ }.not_to change { Event.count } |
|
328 |
+ end |
|
329 |
+ |
|
330 |
+ it "can accept a partial rule set match when 'must_match' is present and less than the total number of rules" do |
|
331 |
+ @checker.options['must_match'] = "1" |
|
332 |
+ |
|
333 |
+ @event.payload['foo']["bing"] = "5" # 5 > 4 |
|
334 |
+ |
|
335 |
+ expect { |
|
336 |
+ @checker.receive([@event]) |
|
337 |
+ }.to change { Event.count }.by(1) |
|
338 |
+ |
|
339 |
+ @event.payload['foo']["bing"] = "2" # 2 !> 4 |
|
340 |
+ |
|
341 |
+ expect { |
|
342 |
+ @checker.receive([@event]) |
|
343 |
+ }.to change { Event.count } # but the first one matches |
|
344 |
+ |
|
345 |
+ |
|
346 |
+ @checker.options['must_match'] = "2" |
|
347 |
+ |
|
348 |
+ @event.payload['foo']["bing"] = "5" # 5 > 4 |
|
349 |
+ |
|
350 |
+ expect { |
|
351 |
+ @checker.receive([@event]) |
|
352 |
+ }.to change { Event.count }.by(1) |
|
353 |
+ |
|
354 |
+ @event.payload['foo']["bing"] = "2" # 2 !> 4 |
|
355 |
+ |
|
356 |
+ expect { |
|
357 |
+ @checker.receive([@event]) |
|
358 |
+ }.not_to change { Event.count } # only 1 matches, we needed 2 |
|
359 |
+ end |
|
303 | 360 |
end |
304 | 361 |
|
305 | 362 |
describe "when 'keep_event' is true" do |
@@ -1,38 +1,86 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::TumblrPublishAgent do |
4 |
- before do |
|
5 |
- @opts = { |
|
6 |
- :blog_name => "huginnbot.tumblr.com", |
|
7 |
- :post_type => "text", |
|
8 |
- :expected_update_period_in_days => "2", |
|
9 |
- :options => { |
|
10 |
- :title => "{{title}}", |
|
11 |
- :body => "{{body}}", |
|
12 |
- }, |
|
13 |
- } |
|
14 |
- |
|
15 |
- @checker = Agents::TumblrPublishAgent.new(:name => "HuginnBot", :options => @opts) |
|
16 |
- @checker.service = services(:generic) |
|
17 |
- @checker.user = users(:bob) |
|
18 |
- @checker.save! |
|
19 |
- |
|
20 |
- @event = Event.new |
|
21 |
- @event.agent = agents(:bob_weather_agent) |
|
22 |
- @event.payload = { :title => "Gonna rain...", :body => 'San Francisco is gonna get wet' } |
|
23 |
- @event.save! |
|
24 |
- |
|
25 |
- stub.any_instance_of(Agents::TumblrPublishAgent).tumblr { |
|
26 |
- stub!.text(anything, anything) { { "id" => "5" } } |
|
27 |
- } |
|
4 |
+ describe "Should create post" do |
|
5 |
+ before do |
|
6 |
+ @opts = { |
|
7 |
+ :blog_name => "huginnbot.tumblr.com", |
|
8 |
+ :post_type => "text", |
|
9 |
+ :expected_update_period_in_days => "2", |
|
10 |
+ :options => { |
|
11 |
+ :title => "{{title}}", |
|
12 |
+ :body => "{{body}}", |
|
13 |
+ }, |
|
14 |
+ } |
|
15 |
+ |
|
16 |
+ @checker = Agents::TumblrPublishAgent.new(:name => "HuginnBot", :options => @opts) |
|
17 |
+ @checker.service = services(:generic) |
|
18 |
+ @checker.user = users(:bob) |
|
19 |
+ @checker.save! |
|
20 |
+ |
|
21 |
+ @event = Event.new |
|
22 |
+ @event.agent = agents(:bob_weather_agent) |
|
23 |
+ @event.payload = { :title => "Gonna rain...", :body => 'San Francisco is gonna get wet' } |
|
24 |
+ @event.save! |
|
25 |
+ |
|
26 |
+ @post_body = { |
|
27 |
+ "id" => 5, |
|
28 |
+ "title" => "Gonna rain...", |
|
29 |
+ "link" => "http://huginnbot.tumblr.com/gonna-rain..." |
|
30 |
+ } |
|
31 |
+ stub.any_instance_of(Agents::TumblrPublishAgent).tumblr { |
|
32 |
+ obj = Object.new |
|
33 |
+ stub(obj).text(anything, anything) { { "id" => "5" } } |
|
34 |
+ stub(obj).posts("huginnbot.tumblr.com", {:id => "5"}) { |
|
35 |
+ {"posts" => [@post_body]} |
|
36 |
+ } |
|
37 |
+ } |
|
38 |
+ |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ describe '#receive' do |
|
42 |
+ it 'should publish any payload it receives' do |
|
43 |
+ Agents::TumblrPublishAgent.async_receive(@checker.id, [@event.id]) |
|
44 |
+ expect(@checker.events.count).to eq(1) |
|
45 |
+ expect(@checker.events.first.payload['post_id']).to eq('5') |
|
46 |
+ expect(@checker.events.first.payload['published_post']).to eq('[huginnbot.tumblr.com] text') |
|
47 |
+ expect(@checker.events.first.payload["post"]).to eq @post_body |
|
48 |
+ end |
|
49 |
+ end |
|
28 | 50 |
end |
29 | 51 |
|
30 |
- describe '#receive' do |
|
31 |
- it 'should publish any payload it receives' do |
|
32 |
- Agents::TumblrPublishAgent.async_receive(@checker.id, [@event.id]) |
|
33 |
- expect(@checker.events.count).to eq(1) |
|
34 |
- expect(@checker.events.first.payload['post_id']).to eq('5') |
|
35 |
- expect(@checker.events.first.payload['published_post']).to eq('[huginnbot.tumblr.com] text') |
|
52 |
+ describe "Should handle tumblr error" do |
|
53 |
+ before do |
|
54 |
+ @opts = { |
|
55 |
+ :blog_name => "huginnbot.tumblr.com", |
|
56 |
+ :post_type => "text", |
|
57 |
+ :expected_update_period_in_days => "2", |
|
58 |
+ :options => { |
|
59 |
+ :title => "{{title}}", |
|
60 |
+ :body => "{{body}}", |
|
61 |
+ }, |
|
62 |
+ } |
|
63 |
+ |
|
64 |
+ @checker = Agents::TumblrPublishAgent.new(:name => "HuginnBot", :options => @opts) |
|
65 |
+ @checker.service = services(:generic) |
|
66 |
+ @checker.user = users(:bob) |
|
67 |
+ @checker.save! |
|
68 |
+ |
|
69 |
+ @event = Event.new |
|
70 |
+ @event.agent = agents(:bob_weather_agent) |
|
71 |
+ @event.payload = { :title => "Gonna rain...", :body => 'San Francisco is gonna get wet' } |
|
72 |
+ @event.save! |
|
73 |
+ |
|
74 |
+ stub.any_instance_of(Agents::TumblrPublishAgent).tumblr { |
|
75 |
+ stub!.text(anything, anything) { {"status" => 401,"msg" => "Not Authorized"} } |
|
76 |
+ } |
|
77 |
+ end |
|
78 |
+ |
|
79 |
+ describe '#receive' do |
|
80 |
+ it 'should publish any payload it receives and handle error' do |
|
81 |
+ Agents::TumblrPublishAgent.async_receive(@checker.id, [@event.id]) |
|
82 |
+ expect(@checker.events.count).to eq(0) |
|
83 |
+ end |
|
36 | 84 |
end |
37 | 85 |
end |
38 | 86 |
end |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::TwilioAgent do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::TwitterPublishAgent do |
4 | 4 |
before do |
@@ -0,0 +1,43 @@ |
||
1 |
+require 'rails_helper' |
|
2 |
+ |
|
3 |
+describe Agents::TwitterSearchAgent do |
|
4 |
+ before do |
|
5 |
+ # intercept the twitter API request |
|
6 |
+ stub_request(:any, /freebandnames/).to_return(body: File.read(Rails.root.join("spec/data_fixtures/search_tweets.json")), status: 200) |
|
7 |
+ |
|
8 |
+ @opts = { |
|
9 |
+ search: "freebandnames", |
|
10 |
+ expected_update_period_in_days: "2", |
|
11 |
+ starting_at: "Jan 01 00:00:01 +0000 2000", |
|
12 |
+ max_results: '3' |
|
13 |
+ } |
|
14 |
+ |
|
15 |
+ end |
|
16 |
+ let(:checker) { |
|
17 |
+ _checker = Agents::TwitterSearchAgent.new(name: "search freebandnames", options: @opts) |
|
18 |
+ _checker.service = services(:generic) |
|
19 |
+ _checker.user = users(:bob) |
|
20 |
+ _checker.save! |
|
21 |
+ _checker |
|
22 |
+ } |
|
23 |
+ |
|
24 |
+ describe "#check" do |
|
25 |
+ it "should check for changes" do |
|
26 |
+ expect { checker.check }.to change { Event.count }.by(3) |
|
27 |
+ end |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+ describe "#check with starting_at=future date" do |
|
31 |
+ it "should check for changes starting_at a future date, thus not find any" do |
|
32 |
+ opts = @opts.merge({ starting_at: "Jan 01 00:00:01 +0000 2999" }) |
|
33 |
+ |
|
34 |
+ checker = Agents::TwitterSearchAgent.new(name: "search freebandnames", options: opts) |
|
35 |
+ checker.service = services(:generic) |
|
36 |
+ checker.user = users(:bob) |
|
37 |
+ checker.save! |
|
38 |
+ |
|
39 |
+ expect { checker.check }.to change { Event.count }.by(0) |
|
40 |
+ end |
|
41 |
+ end |
|
42 |
+ |
|
43 |
+end |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::TwitterStreamAgent do |
4 | 4 |
before do |
@@ -193,6 +193,7 @@ describe Agents::TwitterStreamAgent do |
||
193 | 193 |
context "#stop" do |
194 | 194 |
it "stops the thread" do |
195 | 195 |
mock(@worker.thread).terminate |
196 |
+ mock(@worker.thread).status |
|
196 | 197 |
@worker.stop |
197 | 198 |
end |
198 | 199 |
end |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::TwitterUserAgent do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::UserLocationAgent do |
4 | 4 |
before do |
@@ -0,0 +1,28 @@ |
||
1 |
+require 'rails_helper' |
|
2 |
+ |
|
3 |
+describe Agents::WeatherAgent do |
|
4 |
+ let(:agent) do |
|
5 |
+ Agents::WeatherAgent.create( |
|
6 |
+ name: 'weather', |
|
7 |
+ options: { |
|
8 |
+ :location => 94103, |
|
9 |
+ :lat => 37.779329, |
|
10 |
+ :lng => -122.41915, |
|
11 |
+ :api_key => 'test' |
|
12 |
+ } |
|
13 |
+ ).tap do |agent| |
|
14 |
+ agent.user = users(:bob) |
|
15 |
+ agent.save! |
|
16 |
+ end |
|
17 |
+ end |
|
18 |
+ |
|
19 |
+ it "creates a valid agent" do |
|
20 |
+ expect(agent).to be_valid |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ describe "#service" do |
|
24 |
+ it "doesn't have a Service object attached" do |
|
25 |
+ expect(agent.service).to be_nil |
|
26 |
+ end |
|
27 |
+ end |
|
28 |
+end |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::WebhookAgent do |
4 | 4 |
let(:agent) do |
@@ -30,7 +30,7 @@ describe Agents::WebhookAgent do |
||
30 | 30 |
expect(Event.last.payload).to eq({ 'name' => 'jon' }) |
31 | 31 |
end |
32 | 32 |
|
33 |
- it 'should not create event if secrets dont match' do |
|
33 |
+ it 'should not create event if secrets do not match' do |
|
34 | 34 |
out = nil |
35 | 35 |
expect { |
36 | 36 |
out = agent.receive_web_request({ 'secret' => 'bazbat', 'some_key' => payload }, "post", "text/html") |
@@ -38,6 +38,27 @@ describe Agents::WebhookAgent do |
||
38 | 38 |
expect(out).to eq(['Not Authorized', 401]) |
39 | 39 |
end |
40 | 40 |
|
41 |
+ it 'should respond with customized response message if configured with `response` option' do |
|
42 |
+ agent.options['response'] = 'That Worked' |
|
43 |
+ out = agent.receive_web_request({ 'secret' => 'foobar', 'some_key' => payload }, "post", "text/html") |
|
44 |
+ expect(out).to eq(['That Worked', 201]) |
|
45 |
+ |
|
46 |
+ # Empty string is a valid response |
|
47 |
+ agent.options['response'] = '' |
|
48 |
+ out = agent.receive_web_request({ 'secret' => 'foobar', 'some_key' => payload }, "post", "text/html") |
|
49 |
+ expect(out).to eq(['', 201]) |
|
50 |
+ end |
|
51 |
+ |
|
52 |
+ it 'should respond with `Event Created` if the response option is nil or missing' do |
|
53 |
+ agent.options['response'] = nil |
|
54 |
+ out = agent.receive_web_request({ 'secret' => 'foobar', 'some_key' => payload }, "post", "text/html") |
|
55 |
+ expect(out).to eq(['Event Created', 201]) |
|
56 |
+ |
|
57 |
+ agent.options.delete('response') |
|
58 |
+ out = agent.receive_web_request({ 'secret' => 'foobar', 'some_key' => payload }, "post", "text/html") |
|
59 |
+ expect(out).to eq(['Event Created', 201]) |
|
60 |
+ end |
|
61 |
+ |
|
41 | 62 |
describe "receiving events" do |
42 | 63 |
|
43 | 64 |
context "default settings" do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::WebsiteAgent do |
4 | 4 |
describe "checking without basic auth" do |
@@ -233,7 +233,7 @@ describe Agents::WebsiteAgent do |
||
233 | 233 |
to_return(body: 'hello', |
234 | 234 |
status: 200) |
235 | 235 |
stub_request(:any, /deflate/).with(headers: { 'Accept-Encoding' => /deflate/ }). |
236 |
- to_return(body: '\xcb\x48\xcd\xc9\xc9\x07\x00\x06\x2c'.force_encoding(Encoding::ASCII_8BIT), |
|
236 |
+ to_return(body: "\xcb\x48\xcd\xc9\xc9\x07\x00\x06\x2c".b, |
|
237 | 237 |
headers: { 'Content-Encoding' => 'deflate' }, |
238 | 238 |
status: 200) |
239 | 239 |
|
@@ -262,11 +262,11 @@ describe Agents::WebsiteAgent do |
||
262 | 262 |
describe 'encoding' do |
263 | 263 |
it 'should be forced with force_encoding option' do |
264 | 264 |
huginn = "\u{601d}\u{8003}" |
265 |
- stub_request(:any, /no-encoding/).to_return(:body => { |
|
266 |
- :value => huginn, |
|
267 |
- }.to_json.encode(Encoding::EUC_JP), :headers => { |
|
265 |
+ stub_request(:any, /no-encoding/).to_return(body: { |
|
266 |
+ value: huginn, |
|
267 |
+ }.to_json.encode(Encoding::EUC_JP).b, headers: { |
|
268 | 268 |
'Content-Type' => 'application/json', |
269 |
- }, :status => 200) |
|
269 |
+ }, status: 200) |
|
270 | 270 |
site = { |
271 | 271 |
'name' => "Some JSON Response", |
272 | 272 |
'expected_update_period_in_days' => "2", |
@@ -278,22 +278,22 @@ describe Agents::WebsiteAgent do |
||
278 | 278 |
}, |
279 | 279 |
'force_encoding' => 'EUC-JP', |
280 | 280 |
} |
281 |
- checker = Agents::WebsiteAgent.new(:name => "No Encoding Site", :options => site) |
|
281 |
+ checker = Agents::WebsiteAgent.new(name: "No Encoding Site", options: site) |
|
282 | 282 |
checker.user = users(:bob) |
283 | 283 |
checker.save! |
284 | 284 |
|
285 |
- checker.check |
|
285 |
+ expect { checker.check }.to change { Event.count }.by(1) |
|
286 | 286 |
event = Event.last |
287 | 287 |
expect(event.payload['value']).to eq(huginn) |
288 | 288 |
end |
289 | 289 |
|
290 | 290 |
it 'should be overridden with force_encoding option' do |
291 | 291 |
huginn = "\u{601d}\u{8003}" |
292 |
- stub_request(:any, /wrong-encoding/).to_return(:body => { |
|
293 |
- :value => huginn, |
|
294 |
- }.to_json.encode(Encoding::EUC_JP), :headers => { |
|
292 |
+ stub_request(:any, /wrong-encoding/).to_return(body: { |
|
293 |
+ value: huginn, |
|
294 |
+ }.to_json.encode(Encoding::EUC_JP).b, headers: { |
|
295 | 295 |
'Content-Type' => 'application/json; UTF-8', |
296 |
- }, :status => 200) |
|
296 |
+ }, status: 200) |
|
297 | 297 |
site = { |
298 | 298 |
'name' => "Some JSON Response", |
299 | 299 |
'expected_update_period_in_days' => "2", |
@@ -305,11 +305,63 @@ describe Agents::WebsiteAgent do |
||
305 | 305 |
}, |
306 | 306 |
'force_encoding' => 'EUC-JP', |
307 | 307 |
} |
308 |
- checker = Agents::WebsiteAgent.new(:name => "Wrong Encoding Site", :options => site) |
|
308 |
+ checker = Agents::WebsiteAgent.new(name: "Wrong Encoding Site", options: site) |
|
309 | 309 |
checker.user = users(:bob) |
310 | 310 |
checker.save! |
311 | 311 |
|
312 |
- checker.check |
|
312 |
+ expect { checker.check }.to change { Event.count }.by(1) |
|
313 |
+ event = Event.last |
|
314 |
+ expect(event.payload['value']).to eq(huginn) |
|
315 |
+ end |
|
316 |
+ |
|
317 |
+ it 'should be determined by charset in Content-Type' do |
|
318 |
+ huginn = "\u{601d}\u{8003}" |
|
319 |
+ stub_request(:any, /charset-euc-jp/).to_return(body: { |
|
320 |
+ value: huginn, |
|
321 |
+ }.to_json.encode(Encoding::EUC_JP), headers: { |
|
322 |
+ 'Content-Type' => 'application/json; charset=EUC-JP', |
|
323 |
+ }, status: 200) |
|
324 |
+ site = { |
|
325 |
+ 'name' => "Some JSON Response", |
|
326 |
+ 'expected_update_period_in_days' => "2", |
|
327 |
+ 'type' => "json", |
|
328 |
+ 'url' => "http://charset-euc-jp.example.com", |
|
329 |
+ 'mode' => 'on_change', |
|
330 |
+ 'extract' => { |
|
331 |
+ 'value' => { 'path' => 'value' }, |
|
332 |
+ }, |
|
333 |
+ } |
|
334 |
+ checker = Agents::WebsiteAgent.new(name: "Charset reader", options: site) |
|
335 |
+ checker.user = users(:bob) |
|
336 |
+ checker.save! |
|
337 |
+ |
|
338 |
+ expect { checker.check }.to change { Event.count }.by(1) |
|
339 |
+ event = Event.last |
|
340 |
+ expect(event.payload['value']).to eq(huginn) |
|
341 |
+ end |
|
342 |
+ |
|
343 |
+ it 'should default to UTF-8 when unknown charset is found' do |
|
344 |
+ huginn = "\u{601d}\u{8003}" |
|
345 |
+ stub_request(:any, /charset-unknown/).to_return(body: { |
|
346 |
+ value: huginn, |
|
347 |
+ }.to_json.b, headers: { |
|
348 |
+ 'Content-Type' => 'application/json; charset=unicode', |
|
349 |
+ }, status: 200) |
|
350 |
+ site = { |
|
351 |
+ 'name' => "Some JSON Response", |
|
352 |
+ 'expected_update_period_in_days' => "2", |
|
353 |
+ 'type' => "json", |
|
354 |
+ 'url' => "http://charset-unknown.example.com", |
|
355 |
+ 'mode' => 'on_change', |
|
356 |
+ 'extract' => { |
|
357 |
+ 'value' => { 'path' => 'value' }, |
|
358 |
+ }, |
|
359 |
+ } |
|
360 |
+ checker = Agents::WebsiteAgent.new(name: "Charset reader", options: site) |
|
361 |
+ checker.user = users(:bob) |
|
362 |
+ checker.save! |
|
363 |
+ |
|
364 |
+ expect { checker.check }.to change { Event.count }.by(1) |
|
313 | 365 |
event = Event.last |
314 | 366 |
expect(event.payload['value']).to eq(huginn) |
315 | 367 |
end |
@@ -529,6 +581,41 @@ describe Agents::WebsiteAgent do |
||
529 | 581 |
end |
530 | 582 |
end |
531 | 583 |
|
584 |
+ describe "XML with cdata" do |
|
585 |
+ before do |
|
586 |
+ stub_request(:any, /cdata_rss/).to_return( |
|
587 |
+ body: File.read(Rails.root.join("spec/data_fixtures/cdata_rss.atom")), |
|
588 |
+ status: 200 |
|
589 |
+ ) |
|
590 |
+ |
|
591 |
+ @checker = Agents::WebsiteAgent.new(name: 'cdata', options: { |
|
592 |
+ 'name' => 'CDATA', |
|
593 |
+ 'expected_update_period_in_days' => '2', |
|
594 |
+ 'type' => 'xml', |
|
595 |
+ 'url' => 'http://example.com/cdata_rss.atom', |
|
596 |
+ 'mode' => 'on_change', |
|
597 |
+ 'extract' => { |
|
598 |
+ 'author' => { 'xpath' => '/feed/entry/author/name', 'value' => './/text()'}, |
|
599 |
+ 'title' => { 'xpath' => '/feed/entry/title', 'value' => './/text()' }, |
|
600 |
+ 'content' => { 'xpath' => '/feed/entry/content', 'value' => './/text()' }, |
|
601 |
+ } |
|
602 |
+ }, keep_events_for: 2.days) |
|
603 |
+ @checker.user = users(:bob) |
|
604 |
+ @checker.save! |
|
605 |
+ end |
|
606 |
+ |
|
607 |
+ it "works with XPath" do |
|
608 |
+ expect { |
|
609 |
+ @checker.check |
|
610 |
+ }.to change { Event.count }.by(10) |
|
611 |
+ event = Event.last |
|
612 |
+ expect(event.payload['author']).to eq('bill98') |
|
613 |
+ expect(event.payload['title']).to eq('Help: Rainmeter Skins • Test if Today is Between 2 Dates') |
|
614 |
+ expect(event.payload['content']).to start_with('Can I ') |
|
615 |
+ end |
|
616 |
+ |
|
617 |
+ end |
|
618 |
+ |
|
532 | 619 |
describe "JSON" do |
533 | 620 |
it "works with paths" do |
534 | 621 |
json = { |
@@ -824,4 +911,67 @@ fire: hot |
||
824 | 911 |
end |
825 | 912 |
end |
826 | 913 |
end |
914 |
+ |
|
915 |
+ describe "checking urls" do |
|
916 |
+ before do |
|
917 |
+ stub_request(:any, /example/). |
|
918 |
+ to_return(:body => File.read(Rails.root.join("spec/data_fixtures/urlTest.html")), :status => 200) |
|
919 |
+ @valid_options = { |
|
920 |
+ 'name' => "Url Test", |
|
921 |
+ 'expected_update_period_in_days' => "2", |
|
922 |
+ 'type' => "html", |
|
923 |
+ 'url' => "http://www.example.com", |
|
924 |
+ 'mode' => 'all', |
|
925 |
+ 'extract' => { |
|
926 |
+ 'url' => { 'css' => "a", 'value' => "@href" }, |
|
927 |
+ } |
|
928 |
+ } |
|
929 |
+ @checker = Agents::WebsiteAgent.new(:name => "ua", :options => @valid_options) |
|
930 |
+ @checker.user = users(:bob) |
|
931 |
+ @checker.save! |
|
932 |
+ end |
|
933 |
+ |
|
934 |
+ describe "#check" do |
|
935 |
+ before do |
|
936 |
+ expect { @checker.check }.to change { Event.count }.by(7) |
|
937 |
+ @events = Event.last(7) |
|
938 |
+ end |
|
939 |
+ |
|
940 |
+ it "should check hostname" do |
|
941 |
+ event = @events[0] |
|
942 |
+ expect(event.payload['url']).to eq("http://google.com") |
|
943 |
+ end |
|
944 |
+ |
|
945 |
+ it "should check unescaped query" do |
|
946 |
+ event = @events[1] |
|
947 |
+ expect(event.payload['url']).to eq("https://www.google.ca/search?q=some%20query") |
|
948 |
+ end |
|
949 |
+ |
|
950 |
+ it "should check properly escaped query" do |
|
951 |
+ event = @events[2] |
|
952 |
+ expect(event.payload['url']).to eq("https://www.google.ca/search?q=some%20query") |
|
953 |
+ end |
|
954 |
+ |
|
955 |
+ it "should check unescaped unicode url" do |
|
956 |
+ event = @events[3] |
|
957 |
+ expect(event.payload['url']).to eq("http://ko.wikipedia.org/wiki/%EC%9C%84%ED%82%A4%EB%B0%B1%EA%B3%BC:%EB%8C%80%EB%AC%B8") |
|
958 |
+ end |
|
959 |
+ |
|
960 |
+ it "should check unescaped unicode query" do |
|
961 |
+ event = @events[4] |
|
962 |
+ expect(event.payload['url']).to eq("https://www.google.ca/search?q=%EC%9C%84%ED%82%A4%EB%B0%B1%EA%B3%BC:%EB%8C%80%EB%AC%B8") |
|
963 |
+ end |
|
964 |
+ |
|
965 |
+ it "should check properly escaped unicode url" do |
|
966 |
+ event = @events[5] |
|
967 |
+ expect(event.payload['url']).to eq("http://ko.wikipedia.org/wiki/%EC%9C%84%ED%82%A4%EB%B0%B1%EA%B3%BC:%EB%8C%80%EB%AC%B8") |
|
968 |
+ end |
|
969 |
+ |
|
970 |
+ it "should check properly escaped unicode query" do |
|
971 |
+ event = @events[6] |
|
972 |
+ expect(event.payload['url']).to eq("https://www.google.ca/search?q=%EC%9C%84%ED%82%A4%EB%B0%B1%EA%B3%BC:%EB%8C%80%EB%AC%B8") |
|
973 |
+ end |
|
974 |
+ |
|
975 |
+ end |
|
976 |
+ end |
|
827 | 977 |
end |
@@ -1,5 +1,5 @@ |
||
1 | 1 |
# encoding: utf-8 |
2 |
-require 'spec_helper' |
|
2 |
+require 'rails_helper' |
|
3 | 3 |
|
4 | 4 |
describe Agents::WeiboPublishAgent do |
5 | 5 |
before do |
@@ -1,5 +1,5 @@ |
||
1 | 1 |
# encoding: utf-8 |
2 |
-require 'spec_helper' |
|
2 |
+require 'rails_helper' |
|
3 | 3 |
|
4 | 4 |
describe Agents::WeiboUserAgent do |
5 | 5 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Agents::WitaiAgent do |
4 | 4 |
before do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
require 'models/concerns/oauthable' |
3 | 3 |
|
4 | 4 |
describe Agents::WunderlistAgent do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
module Agents |
4 | 4 |
class OauthableTestAgent < Agent |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Event do |
4 | 4 |
describe ".with_location" do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe ScenarioImport do |
4 | 4 |
let(:user) { users(:bob) } |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Scenario do |
4 | 4 |
let(:new_instance) { users(:bob).scenarios.build(:name => "some scenario") } |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe Service do |
4 | 4 |
before(:each) do |
@@ -1,19 +1,19 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe UserCredential do |
4 | 4 |
describe "validation" do |
5 |
- it { is_expected.to validate_uniqueness_of(:credential_name).scoped_to(:user_id) } |
|
6 |
- it { is_expected.to validate_presence_of(:credential_name) } |
|
7 |
- it { is_expected.to validate_presence_of(:credential_value) } |
|
8 |
- it { is_expected.to validate_presence_of(:user_id) } |
|
5 |
+ it { should validate_uniqueness_of(:credential_name).scoped_to(:user_id) } |
|
6 |
+ it { should validate_presence_of(:credential_name) } |
|
7 |
+ it { should validate_presence_of(:credential_value) } |
|
8 |
+ it { should validate_presence_of(:user_id) } |
|
9 | 9 |
end |
10 | 10 |
|
11 | 11 |
describe "mass assignment" do |
12 |
- it { is_expected.to allow_mass_assignment_of :credential_name } |
|
12 |
+ it { should allow_mass_assignment_of :credential_name } |
|
13 | 13 |
|
14 |
- it { is_expected.to allow_mass_assignment_of :credential_value } |
|
14 |
+ it { should allow_mass_assignment_of :credential_value } |
|
15 | 15 |
|
16 |
- it { is_expected.not_to allow_mass_assignment_of :user_id } |
|
16 |
+ it { should_not allow_mass_assignment_of :user_id } |
|
17 | 17 |
end |
18 | 18 |
|
19 | 19 |
describe "cleaning fields" do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe User do |
4 | 4 |
describe "validations" do |
@@ -10,13 +10,13 @@ describe User do |
||
10 | 10 |
|
11 | 11 |
it "only accepts valid invitation codes" do |
12 | 12 |
User::INVITATION_CODES.each do |v| |
13 |
- is_expected.to allow_value(v).for(:invitation_code) |
|
13 |
+ should allow_value(v).for(:invitation_code) |
|
14 | 14 |
end |
15 | 15 |
end |
16 | 16 |
|
17 | 17 |
it "can reject invalid invitation codes" do |
18 | 18 |
%w['foo', 'bar'].each do |v| |
19 |
- is_expected.not_to allow_value(v).for(:invitation_code) |
|
19 |
+ should_not allow_value(v).for(:invitation_code) |
|
20 | 20 |
end |
21 | 21 |
end |
22 | 22 |
end |
@@ -28,7 +28,7 @@ describe User do |
||
28 | 28 |
|
29 | 29 |
it "skips this validation" do |
30 | 30 |
%w['foo', 'bar', nil, ''].each do |v| |
31 |
- is_expected.to allow_value(v).for(:invitation_code) |
|
31 |
+ should allow_value(v).for(:invitation_code) |
|
32 | 32 |
end |
33 | 33 |
end |
34 | 34 |
end |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe FormConfigurableAgentPresenter do |
4 | 4 |
include RSpecHtmlMatchers |
@@ -21,6 +21,14 @@ Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } |
||
21 | 21 |
|
22 | 22 |
ActiveRecord::Migration.maintain_test_schema! |
23 | 23 |
|
24 |
+# Mix in shoulda matchers |
|
25 |
+Shoulda::Matchers.configure do |config| |
|
26 |
+ config.integrate do |with| |
|
27 |
+ with.test_framework :rspec |
|
28 |
+ with.library :rails |
|
29 |
+ end |
|
30 |
+end |
|
31 |
+ |
|
24 | 32 |
RSpec.configure do |config| |
25 | 33 |
config.mock_with :rr |
26 | 34 |
|
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
describe "routing for web requests", :type => :routing do |
4 | 4 |
it "routes to handle_request" do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
shared_examples_for AgentControllerConcern do |
4 | 4 |
describe "preconditions" do |
@@ -128,5 +128,24 @@ shared_examples_for AgentControllerConcern do |
||
128 | 128 |
expect(agent.control_targets.reload).to all(satisfy { |a| a.options['url'] == 'http://some-new-url.com/SOMETHING' }) |
129 | 129 |
expect(agents(:bob_website_agent).reload.options).to eq(old_options.merge('url' => 'http://some-new-url.com/SOMETHING')) |
130 | 130 |
end |
131 |
+ |
|
132 |
+ it "should configure targets with nested objects" do |
|
133 |
+ agent.control_targets << agents(:bob_data_output_agent) |
|
134 |
+ agent.options['action'] = 'configure' |
|
135 |
+ agent.options['configure_options'] = { |
|
136 |
+ template: { |
|
137 |
+ item: { |
|
138 |
+ title: "changed" |
|
139 |
+ } |
|
140 |
+ } |
|
141 |
+ } |
|
142 |
+ agent.save! |
|
143 |
+ old_options = agents(:bob_data_output_agent).options |
|
144 |
+ |
|
145 |
+ agent.control! |
|
146 |
+ |
|
147 |
+ expect(agent.control_targets.reload).to all(satisfy { |a| a.options['template'] && a.options['template']['item'] && (a.options['template']['item']['title'] == 'changed') }) |
|
148 |
+ expect(agents(:bob_data_output_agent).reload.options).to eq(old_options.deep_merge(agent.options['configure_options'])) |
|
149 |
+ end |
|
131 | 150 |
end |
132 | 151 |
end |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
shared_examples_for EmailConcern do |
4 | 4 |
let(:valid_options) { |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
shared_examples_for HasGuid do |
4 | 4 |
it "gets created before_save, but only if it's not present" do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
shared_examples_for LiquidInterpolatable do |
4 | 4 |
before(:each) do |
@@ -94,7 +94,7 @@ shared_examples_for LiquidInterpolatable do |
||
94 | 94 |
it "should raise an exception for undefined credentials" do |
95 | 95 |
expect { |
96 | 96 |
@checker.interpolate_string("{% credential unknown %}", {}) |
97 |
- }.to raise_error |
|
97 |
+ }.to raise_error(/No user credential named/) |
|
98 | 98 |
end |
99 | 99 |
end |
100 | 100 |
|
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
shared_examples_for WebRequestConcern do |
4 | 4 |
let(:agent) do |
@@ -1,4 +1,4 @@ |
||
1 |
-require 'spec_helper' |
|
1 |
+require 'rails_helper' |
|
2 | 2 |
|
3 | 3 |
shared_examples_for WorkingHelpers do |
4 | 4 |
describe "recent_error_logs?" do |