Skip to content

Commit 0b75767

Browse files
committed
DataOutputAgent: Add support for publishing via PubSubHubbub
A new option `push_hubs` is added, which lists PuSH hub endpoints to publish updates to.
1 parent c03a60e commit 0b75767

File tree

2 files changed

+116
-22
lines changed

2 files changed

+116
-22
lines changed

app/models/agents/data_output_agent.rb

Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module Agents
22
class DataOutputAgent < Agent
3+
include WebRequestConcern
4+
35
cannot_be_scheduled!
46

57
description do
@@ -22,6 +24,7 @@ class DataOutputAgent < Agent
2224
* `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.
2325
* `events_to_show` - The number of events to output in RSS or JSON. (default: `40`)
2426
* `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 every update to. (default: none) Note that publishing updates will make your feed public. Popular hubs include [Superfeedr](https://pubsubhubbub.superfeedr.com/) and [Google](https://pubsubhubbub.appspot.com/).
2528
2629
If you'd like to output RSS tags with attributes, such as `enclosure`, use something like the following in your `template`:
2730
@@ -95,6 +98,29 @@ def validate_options
9598
unless options['template'].present? && options['template']['item'].present? && options['template']['item'].is_a?(Hash)
9699
errors.add(:base, "Please provide template and template.item")
97100
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
98124
end
99125

100126
def events_to_show
@@ -130,6 +156,10 @@ def feed_description
130156
interpolated['template']['description'].presence || "A feed of Events received by the '#{name}' Huginn Agent"
131157
end
132158

159+
def push_hubs
160+
interpolated['push_hubs'].presence || []
161+
end
162+
133163
def receive_web_request(params, method, format)
134164
unless interpolated['secrets'].include?(params['secret'])
135165
if format =~ /json/
@@ -160,40 +190,54 @@ def receive_web_request(params, method, format)
160190
interpolated
161191
end
162192

193+
now = Time.now
194+
163195
if format =~ /json/
164196
content = {
165197
'title' => feed_title,
166198
'description' => feed_description,
167-
'pubDate' => Time.now,
199+
'pubDate' => now,
168200
'items' => simplify_item_for_json(items)
169201
}
170202

171203
return [content, 200]
172204
else
173-
content = Utils.unindent(<<-XML)
174-
<?xml version="1.0" encoding="UTF-8" ?>
175-
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
176-
<channel>
177-
<atom:link href=#{feed_url(secret: params['secret'], format: :xml).encode(xml: :attr)} rel="self" type="application/rss+xml" />
178-
<atom:icon>#{feed_icon.encode(xml: :text)}</atom:icon>
179-
<title>#{feed_title.encode(xml: :text)}</title>
180-
<description>#{feed_description.encode(xml: :text)}</description>
181-
<link>#{feed_link.encode(xml: :text)}</link>
182-
<lastBuildDate>#{Time.now.rfc2822.to_s.encode(xml: :text)}</lastBuildDate>
183-
<pubDate>#{Time.now.rfc2822.to_s.encode(xml: :text)}</pubDate>
184-
<ttl>#{feed_ttl}</ttl>
185-
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">
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>
186231
XML
232+
end
233+
end
234+
end
187235

188-
content += simplify_item_for_xml(items).to_xml(skip_types: true, root: "items", skip_instruct: true, indent: 1).gsub(/^<\/?items>/, '').strip
189-
190-
content += Utils.unindent(<<-XML)
191-
</channel>
192-
</rss>
193-
XML
236+
def receive(incoming_events)
237+
url = feed_url(secret: interpolated['secrets'].first, format: :xml)
194238

195-
return [content, 200, 'text/xml']
196-
end
239+
push_hubs.each do |hub|
240+
push_to_hub(hub, url)
197241
end
198242
end
199243

@@ -262,5 +306,32 @@ def simplify_item_for_json(item)
262306
item
263307
end
264308
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
265336
end
266337
end

spec/models/agents/data_output_agent_spec.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,29 @@
7373
end
7474
end
7575

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+
7699
describe "#receive_web_request" do
77100
before do
78101
current_time = Time.now

0 commit comments

Comments
 (0)