hootenanny 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +5 -7
- data/Gemfile.lock +46 -28
- data/app/controllers/hootenanny/notifications_controller.rb +31 -0
- data/app/controllers/hootenanny/parameters.rb +16 -0
- data/app/controllers/hootenanny/subscriptions_controller.rb +24 -3
- data/app/models/hootenanny/publish_notification.rb +90 -0
- data/app/models/hootenanny/subscription.rb +72 -23
- data/config/routes.rb +2 -1
- data/db/migrate/20130607182642_add_started_at_and_lease_duration_to_subscriptions.rb +15 -0
- data/db/migrate/20130608225621_add_hmac_secret_to_subscription.rb +5 -0
- data/db/migrate/20130611235218_add_publish_notifications.rb +9 -0
- data/db/migrate/20130612153138_add_timestamps_to_publish_notification.rb +5 -0
- data/db/migrate/20130705200729_add_processed_flag_to_publish_notifications.rb +6 -0
- data/db/migrate/20130711061329_switch_publish_notification_process_state_from_boolean_to_string.rb +11 -0
- data/db/migrate/20130711061558_add_index_to_notifications_state.rb +5 -0
- data/hootenanny.gemspec +6 -2
- data/lib/hootenanny/configuration.rb +43 -0
- data/lib/hootenanny/correspondent.rb +44 -0
- data/lib/hootenanny/errors.rb +42 -1
- data/lib/hootenanny/feed.rb +75 -0
- data/lib/hootenanny/feed/atom_feed.rb +18 -0
- data/lib/hootenanny/feed/atom_feed_item.rb +8 -0
- data/lib/hootenanny/feed/digest_feed.rb +14 -0
- data/lib/hootenanny/feed/digest_feed_item.rb +11 -0
- data/lib/hootenanny/feed/feed_item.rb +30 -0
- data/lib/hootenanny/feed/file.rb +66 -0
- data/lib/hootenanny/feed/json_feed.rb +48 -0
- data/lib/hootenanny/feed/json_feed_item.rb +8 -0
- data/lib/hootenanny/feed/null_feed.rb +27 -0
- data/lib/hootenanny/feed/rss_feed.rb +52 -0
- data/lib/hootenanny/feed/rss_feed_item.rb +8 -0
- data/lib/hootenanny/feed_store.rb +30 -0
- data/lib/hootenanny/feed_store/file_feed_store.rb +55 -0
- data/lib/hootenanny/feed_store/web_feed_store.rb +42 -0
- data/lib/hootenanny/hub.rb +116 -22
- data/lib/hootenanny/publish_notification_expiration_policy.rb +17 -0
- data/lib/hootenanny/request.rb +40 -0
- data/lib/hootenanny/request/publish_notification.rb +94 -0
- data/lib/hootenanny/request/subscription.rb +153 -0
- data/lib/hootenanny/subscription_delivery.rb +47 -0
- data/lib/hootenanny/topic.rb +38 -0
- data/lib/hootenanny/topic_synchronizer.rb +71 -0
- data/lib/hootenanny/uri.rb +53 -0
- data/lib/hootenanny/verification.rb +108 -0
- data/lib/hootenanny/version.rb +1 -1
- data/spec/dummy/config/database.yml +12 -6
- data/spec/dummy/db/migrate/20130607183149_add_started_at_and_lease_duration_to_subscriptions.hootenanny.rb +16 -0
- data/spec/dummy/db/migrate/20130608231253_add_hmac_secret_to_subscription.hootenanny.rb +6 -0
- data/spec/dummy/db/migrate/20130611235546_add_publish_notifications.hootenanny.rb +10 -0
- data/spec/dummy/db/migrate/20130612153353_add_timestamps_to_publish_notification.hootenanny.rb +6 -0
- data/spec/dummy/db/migrate/20130705200832_add_processed_flag_to_publish_notifications.hootenanny.rb +7 -0
- data/spec/dummy/db/migrate/20130711061518_switch_publish_notification_process_state_from_boolean_to_string.hootenanny.rb +12 -0
- data/spec/dummy/db/migrate/20130711061629_add_index_to_notifications_state.hootenanny.rb +6 -0
- data/spec/factories/publish_notification.rb +13 -0
- data/spec/factories/requests/publish_notification.rb +11 -0
- data/spec/factories/requests/subscription.rb +46 -0
- data/spec/factories/subscription.rb +14 -2
- data/spec/factories/verification.rb +15 -0
- data/spec/features/publishers/can_notify_the_hub_of_content_updates_spec.rb +58 -0
- data/spec/features/subscribers/are_protected_from_unwarranted_subscriptions_spec.rb +59 -0
- data/spec/features/subscribers/can_receive_distributions_of_topic_content_spec.rb +175 -0
- data/spec/features/subscribers/can_subscribe_to_a_topic_spec.rb +76 -7
- data/spec/fixtures/feeds/atom/97d79220f68b4bf27.atom +0 -0
- data/spec/fixtures/feeds/atom/sample_feed.atom +51 -0
- data/spec/fixtures/feeds/digest/97d79220f68b4bf27.digest +1 -0
- data/spec/fixtures/feeds/digest/complete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +6 -0
- data/spec/fixtures/feeds/digest/incomplete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +5 -0
- data/spec/fixtures/feeds/digest/sample_feed.digest +6 -0
- data/spec/fixtures/feeds/json/97d79220f68b4bf27.json +1 -0
- data/spec/fixtures/feeds/json/feed_with_one_item.json +21 -0
- data/spec/fixtures/feeds/json/sample_feed.json +30 -0
- data/spec/fixtures/feeds/rss/5b187098da59f077f/97d79220f68b4bf27.rss +21 -0
- data/spec/fixtures/feeds/rss/97d79220f68b4bf27.rss +0 -0
- data/spec/fixtures/feeds/rss/feed_with_one_item.rss +15 -0
- data/spec/fixtures/feeds/rss/minimal_feed.rss +12 -0
- data/spec/fixtures/feeds/rss/sample_feed.rss +22 -0
- data/spec/fixtures/feeds/rss/sample_feed_2.rss +22 -0
- data/spec/lib/hootenanny/configuration_spec.rb +7 -0
- data/spec/lib/hootenanny/correspondent_spec.rb +94 -0
- data/spec/lib/hootenanny/errors_spec.rb +21 -0
- data/spec/lib/hootenanny/feed/atom_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/atom_feed_spec.rb +40 -0
- data/spec/lib/hootenanny/feed/digest_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/digest_feed_spec.rb +40 -0
- data/spec/lib/hootenanny/feed/feed_item_spec.rb +49 -0
- data/spec/lib/hootenanny/feed/file_spec.rb +66 -0
- data/spec/lib/hootenanny/feed/json_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/json_feed_spec.rb +128 -0
- data/spec/lib/hootenanny/feed/null_feed_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/rss_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/rss_feed_spec.rb +143 -0
- data/spec/lib/hootenanny/feed_spec.rb +159 -0
- data/spec/lib/hootenanny/feed_store/file_feed_store_spec.rb +58 -0
- data/spec/lib/hootenanny/feed_store/web_feed_store_spec.rb +47 -0
- data/spec/lib/hootenanny/feed_store_spec.rb +27 -0
- data/spec/lib/hootenanny/hub_spec.rb +73 -0
- data/spec/lib/hootenanny/publish_notification_expiration_policy_spec.rb +35 -0
- data/spec/lib/hootenanny/request/publish_notification_spec.rb +43 -0
- data/spec/lib/hootenanny/request/subscription_spec.rb +89 -0
- data/spec/lib/hootenanny/request_spec.rb +21 -0
- data/spec/lib/hootenanny/subscription_delivery_spec.rb +54 -0
- data/spec/lib/hootenanny/topic_spec.rb +15 -0
- data/spec/lib/hootenanny/topic_synchronizer_spec.rb +98 -0
- data/spec/lib/hootenanny/uri_spec.rb +32 -0
- data/spec/lib/hootenanny/verification_spec.rb +92 -0
- data/spec/models/hootenanny/publish_notification_spec.rb +55 -0
- data/spec/models/hootenanny/subscription_spec.rb +58 -19
- data/spec/support/verification.rb +63 -0
- metadata +231 -14
- data/spec/controllers/hootenanny/hub_spec.rb +0 -15
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'hootenanny/request/subscription'
|
3
|
+
require 'hootenanny/request/publish_notification'
|
4
|
+
|
5
|
+
module Hootenanny
|
6
|
+
class Request
|
7
|
+
attr_accessor :type
|
8
|
+
|
9
|
+
###
|
10
|
+
# Private: Builds a specific request object based on the options passed in.
|
11
|
+
#
|
12
|
+
# options - A Hash of options representing the request that will be built. The
|
13
|
+
# only option that is required for _this_ method is :type, however
|
14
|
+
# depending on the class that is to be built, it may require other
|
15
|
+
# options. See each class in turn for details.
|
16
|
+
#
|
17
|
+
# :type - The type of request that should be built. Currently, valid
|
18
|
+
# opitons are:
|
19
|
+
#
|
20
|
+
# * :subscription
|
21
|
+
#
|
22
|
+
# Examples:
|
23
|
+
#
|
24
|
+
# Hootenanny::Request.build(type: :subscription,
|
25
|
+
# callback: 'http://example.com',)
|
26
|
+
# # => <Hootenanny::Request::Subscription>
|
27
|
+
#
|
28
|
+
# Returns an indeterminate type of object based on the :type option
|
29
|
+
# Raises Hootenanny::Request::BuildError if there is a problem
|
30
|
+
#
|
31
|
+
def self.build(options = {})
|
32
|
+
type = options.fetch(:type).to_s.camelize
|
33
|
+
klass = "Hootenanny::Request::#{type}".constantize
|
34
|
+
|
35
|
+
klass.build options
|
36
|
+
rescue KeyError => e
|
37
|
+
raise Hootenanny::Request::BuildError.wrap(e)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'active_support/core_ext/array/wrap'
|
2
|
+
require 'hootenanny/request'
|
3
|
+
require 'hootenanny/publish_notification'
|
4
|
+
|
5
|
+
module Hootenanny
|
6
|
+
class Request
|
7
|
+
class PublishNotification < Hootenanny::Request
|
8
|
+
|
9
|
+
attr_reader :topics
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
self.topics = options.fetch(:topics)
|
13
|
+
rescue KeyError => e
|
14
|
+
raise Hootenanny::Request::BuildError.wrap(e)
|
15
|
+
end
|
16
|
+
|
17
|
+
###
|
18
|
+
# Private: Builds a publication request object based on the options passed in.
|
19
|
+
#
|
20
|
+
# options - A Hash of options representing the publish notification request
|
21
|
+
# that will be built.
|
22
|
+
#
|
23
|
+
# :topics - A String or Array representing the topics which have
|
24
|
+
# been proposed to have been updated.
|
25
|
+
#
|
26
|
+
# Examples:
|
27
|
+
#
|
28
|
+
# # When more than one topic is passed in
|
29
|
+
#
|
30
|
+
# Hootenanny::Request::PublishNotification.build(topics: [
|
31
|
+
# 'http://another.org',
|
32
|
+
# 'http://other.com'
|
33
|
+
# ])
|
34
|
+
# # => <Hootenanny::Request::PublishNotification topics: [
|
35
|
+
# 'http://another.org',
|
36
|
+
# 'http://other.com'
|
37
|
+
# ]>
|
38
|
+
#
|
39
|
+
#
|
40
|
+
# # When only one topic is passed in
|
41
|
+
#
|
42
|
+
# Hootenanny::Request::PublishNotification.build(topics: 'http://another.org')
|
43
|
+
# # => <Hootenanny::Request::Publish topics: ['http://another.org']>
|
44
|
+
#
|
45
|
+
# Returns a Hootenanny::Request::PublishNotification
|
46
|
+
# Raises Hootenanny::Request::BuildError if there is a problem
|
47
|
+
#
|
48
|
+
def self.build(options = {})
|
49
|
+
options = normalize_build_options(options)
|
50
|
+
request = allocate
|
51
|
+
|
52
|
+
request.send(:initialize, options)
|
53
|
+
|
54
|
+
request
|
55
|
+
end
|
56
|
+
|
57
|
+
###
|
58
|
+
# Private: Generates one ore more (one for each topic in the request) records
|
59
|
+
# of the publish notification to be consumed at a later time by the hub. If
|
60
|
+
# the hub has already been notified of the content update, a second item is
|
61
|
+
# not created but rather the original one is retrieved and returned.
|
62
|
+
#
|
63
|
+
# Example:
|
64
|
+
#
|
65
|
+
# Hootenanny::Request::PublishNotification.build(topics: 'http://another.org')
|
66
|
+
#
|
67
|
+
# publish_notification_request.apply
|
68
|
+
#
|
69
|
+
# # => <Hootenanny::PublishNotification topic: 'http://another.org'>
|
70
|
+
#
|
71
|
+
# Returns a Hootenanny::PublishNotification if the request is verified
|
72
|
+
# Raises Hootenanny::PublishNotificationError if there is a problem
|
73
|
+
#
|
74
|
+
def apply
|
75
|
+
topics.map do |topic|
|
76
|
+
Hootenanny::PublishNotification.notify(topic)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
attr_writer :topics
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def self.normalize_build_options(options)
|
87
|
+
options[:topics] = options[:url] if options[:url]
|
88
|
+
options[:topics] = Array.wrap(options[:topics]) if options[:topics]
|
89
|
+
|
90
|
+
options
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'hootenanny/request'
|
2
|
+
require 'hootenanny/subscription'
|
3
|
+
require 'hootenanny/verification'
|
4
|
+
|
5
|
+
module Hootenanny
|
6
|
+
class Request
|
7
|
+
class Subscription < Hootenanny::Request
|
8
|
+
|
9
|
+
attr_reader :subscriber,
|
10
|
+
:topic,
|
11
|
+
:lease_duration,
|
12
|
+
:requester_token,
|
13
|
+
:digest_secret,
|
14
|
+
:verification_class
|
15
|
+
|
16
|
+
def initialize(options = {})
|
17
|
+
self.subscriber = options.fetch(:subscriber)
|
18
|
+
self.topic = options.fetch(:topic)
|
19
|
+
self.lease_duration = options.fetch(:lease_duration)
|
20
|
+
self.requester_token = options.fetch(:requester_token, nil)
|
21
|
+
self.digest_secret = options.fetch(:secret, nil)
|
22
|
+
self.verification_class = options.fetch(:verification_class,
|
23
|
+
Hootenanny::Verification)
|
24
|
+
rescue KeyError => e
|
25
|
+
raise Hootenanny::Request::BuildError.wrap(e)
|
26
|
+
end
|
27
|
+
|
28
|
+
###
|
29
|
+
# Private: Builds a subscription request object based on the options passed
|
30
|
+
# in.
|
31
|
+
#
|
32
|
+
# options - A Hash of options representing the subscription request that will
|
33
|
+
# be built.
|
34
|
+
#
|
35
|
+
# :subscriber - A String or Ruby URI representing the URI which
|
36
|
+
# the topic updates will be sent to.
|
37
|
+
# :topic - A String or Ruby URI representing the URI which
|
38
|
+
# publish the activities for a given topic.
|
39
|
+
# :requester_token - A String token sent from the requester which
|
40
|
+
# they can use to identify the request on their
|
41
|
+
# system (optional).
|
42
|
+
# :digest_secret - A String token of no more than 200 bytes which
|
43
|
+
# will be used during content distribution so
|
44
|
+
# that the subscriber can verify that the request
|
45
|
+
# is from the hub and not a undesired 3rd party
|
46
|
+
# (optional).
|
47
|
+
# :lease_duration - A Number representing the amount of time after
|
48
|
+
# the subscription is created during which time
|
49
|
+
# the subscription will be active. This is
|
50
|
+
# a _request_ and may be overridden (optional).
|
51
|
+
#
|
52
|
+
# Examples:
|
53
|
+
#
|
54
|
+
# Hootenanny::Request::Subscription.build(callback: 'http://example.com',
|
55
|
+
# topic: 'http://another.org')
|
56
|
+
#
|
57
|
+
# # => <Hootenanny::Request::Subscription subscriber: 'http://example.com',
|
58
|
+
# topic: 'http://another.org'>
|
59
|
+
#
|
60
|
+
# Returns a Hootenanny::Request::Subscription
|
61
|
+
# Raises Hootenanny::Request::BuildError if there is a problem
|
62
|
+
#
|
63
|
+
def self.build(options = {})
|
64
|
+
options = normalize_build_options(options)
|
65
|
+
request = allocate
|
66
|
+
|
67
|
+
request.send(:initialize, options)
|
68
|
+
|
69
|
+
request
|
70
|
+
end
|
71
|
+
|
72
|
+
###
|
73
|
+
# Private: Applies the subscription that the request is asking for. It does
|
74
|
+
# this in an idempotent way. If the subscription being requested already
|
75
|
+
# exists, it is not modified but is instead retrieved and returned.
|
76
|
+
#
|
77
|
+
# The request itself must be verified prior to the request being applied. An
|
78
|
+
# unverifiable request will raise a Hootenanny::SubscriptionError.
|
79
|
+
#
|
80
|
+
# Example:
|
81
|
+
#
|
82
|
+
# # If the request for the subscription is verified
|
83
|
+
#
|
84
|
+
# subscription_request = Hootenanny::Request::Subscription.build(
|
85
|
+
# callback: 'http://example.com',
|
86
|
+
# topic: 'http://another.org')
|
87
|
+
#
|
88
|
+
# subscription_request.apply
|
89
|
+
#
|
90
|
+
# # => <Hootenanny::Subscription subscriber: 'http://example.com',
|
91
|
+
# topic: 'http://another.org'>
|
92
|
+
#
|
93
|
+
#
|
94
|
+
#
|
95
|
+
# # If the request for the subscription is unverifiable
|
96
|
+
#
|
97
|
+
# subscription_request = Hootenanny::Request::Subscription.build(
|
98
|
+
# callback: 'http://iamahacker.com',
|
99
|
+
# topic: 'http://thisdoesnotexist.org')
|
100
|
+
#
|
101
|
+
# subscription_request.apply
|
102
|
+
#
|
103
|
+
# # => <Hootenanny::SubscriptionError>
|
104
|
+
#
|
105
|
+
# Returns a Hootenanny::Subscription if the request is verified
|
106
|
+
# Raises Hootenanny::SubscriptionError if there is a problem
|
107
|
+
#
|
108
|
+
def apply
|
109
|
+
raise Hootenanny::SubscriptionError.new('Request could not be verified.') unless verified?
|
110
|
+
|
111
|
+
Hootenanny::Subscription.subscribe( subscriber: subscriber,
|
112
|
+
to: topic,
|
113
|
+
lease_duration: lease_duration,
|
114
|
+
digest_secret: digest_secret)
|
115
|
+
end
|
116
|
+
|
117
|
+
def verified?
|
118
|
+
verification.verified?
|
119
|
+
end
|
120
|
+
|
121
|
+
def verification
|
122
|
+
@verification ||= verification_class.new(
|
123
|
+
url: subscriber,
|
124
|
+
mode: :subscribe,
|
125
|
+
topic: topic,
|
126
|
+
requester_token: requester_token,
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
130
|
+
protected
|
131
|
+
|
132
|
+
attr_writer :subscriber,
|
133
|
+
:topic,
|
134
|
+
:lease_duration,
|
135
|
+
:requester_token,
|
136
|
+
:digest_secret,
|
137
|
+
:verification_class
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def self.normalize_build_options(options)
|
142
|
+
options[:subscriber] = options[:callback] if options.has_key?(:callback)
|
143
|
+
options[:lease_duration] = options[:lease_seconds] if options.has_key?(:lease_seconds)
|
144
|
+
|
145
|
+
options[:lease_duration] = options[:lease_duration] ?
|
146
|
+
options[:lease_duration].to_i :
|
147
|
+
1_577_880_000 # 50 Year Duration = 60 * 60 * 24 * 365.25 * 50
|
148
|
+
|
149
|
+
options
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'digest'
|
3
|
+
|
4
|
+
module Hootenanny
|
5
|
+
class SubscriptionDelivery
|
6
|
+
def initialize(options = {})
|
7
|
+
self.subscriber_url = options.fetch(:subscriber)
|
8
|
+
self.digest_secret = options.fetch(:digest_secret, nil)
|
9
|
+
self.feed = options.fetch(:feed)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.deliver(options = {})
|
13
|
+
delivery = allocate
|
14
|
+
delivery.send(:initialize, options)
|
15
|
+
|
16
|
+
delivery.deliver
|
17
|
+
end
|
18
|
+
|
19
|
+
def deliver
|
20
|
+
self.response = Faraday.post subscriber_url.to_s do |request|
|
21
|
+
request.headers['Content-Type'] = feed.content_type
|
22
|
+
request.headers['X-Hub-Signature'] = "sha1=#{signature}" unless signature == ''
|
23
|
+
request.body = feed.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
self.successful = response.success?
|
27
|
+
end
|
28
|
+
|
29
|
+
def successful?
|
30
|
+
successful
|
31
|
+
end
|
32
|
+
|
33
|
+
def signature
|
34
|
+
return '' if digest_secret == '' or digest_secret.nil?
|
35
|
+
|
36
|
+
@signature ||= OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA'), digest_secret, feed.to_s)
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
attr_accessor :subscriber_url,
|
42
|
+
:digest_secret,
|
43
|
+
:feed,
|
44
|
+
:response,
|
45
|
+
:successful
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'hootenanny/uri'
|
2
|
+
|
3
|
+
module Hootenanny
|
4
|
+
class Topic
|
5
|
+
|
6
|
+
attr_reader :url
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
self.url = Hootenanny::URI.parse(options.fetch(:url))
|
10
|
+
end
|
11
|
+
|
12
|
+
###
|
13
|
+
# Private: Creates a topic from a String representing the URL of the topic
|
14
|
+
#
|
15
|
+
# url - A String representing the URL of the topic
|
16
|
+
#
|
17
|
+
# Examples:
|
18
|
+
#
|
19
|
+
# Hootenanny::Topic.from_url('http://example.com')
|
20
|
+
#
|
21
|
+
# Returns a Hootenanny::Topic
|
22
|
+
# Raises a Hootenanny::URI::InvalidError if the URI cannot be parsed
|
23
|
+
# Raises a Hootenanny::URI::InvalidSchemeError if the URI is not either HTTP
|
24
|
+
# or HTTPS
|
25
|
+
#
|
26
|
+
def self.from_url(url, options = {})
|
27
|
+
topic = allocate
|
28
|
+
|
29
|
+
topic.send(:initialize, options.merge(:url => url))
|
30
|
+
|
31
|
+
topic
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
attr_writer :url
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'hootenanny/configuration'
|
2
|
+
|
3
|
+
module Hootenanny
|
4
|
+
class TopicSynchronizer
|
5
|
+
def initialize(options = {})
|
6
|
+
self.subscriber_url = options.fetch(:subscriber)
|
7
|
+
self.topic_url = options.fetch(:topic)
|
8
|
+
self.digest_secret = options.fetch(:digest_secret, nil)
|
9
|
+
|
10
|
+
self.local_feed_store = options[:local_feed_store] ||
|
11
|
+
Hootenanny.config.default_local_feed_store
|
12
|
+
self.remote_feed_store = options[:remote_feed_store] ||
|
13
|
+
Hootenanny.config.default_remote_feed_store
|
14
|
+
self.subscription_delivery = options[:subscription_delivery] ||
|
15
|
+
Hootenanny.config.default_subscription_delivery
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.sync(options = {})
|
19
|
+
self.new(options).sync
|
20
|
+
end
|
21
|
+
|
22
|
+
def sync
|
23
|
+
return true if unsynchronized_feed.empty?
|
24
|
+
|
25
|
+
if subscription_delivery.deliver( feed: unsynchronized_feed,
|
26
|
+
url: topic_url,
|
27
|
+
subscriber: subscriber_url)
|
28
|
+
|
29
|
+
local_feed_store.store( feed: remote_feed.to_digest_feed,
|
30
|
+
url: topic_url,
|
31
|
+
type: 'digest',
|
32
|
+
path: subscriber_url.to_digest)
|
33
|
+
else
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
attr_accessor :subscriber_url,
|
41
|
+
:topic_url,
|
42
|
+
:digest_secret,
|
43
|
+
:local_feed_store,
|
44
|
+
:remote_feed_store,
|
45
|
+
:subscription_delivery
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def unsynchronized_feed
|
50
|
+
remote_feed - local_feed
|
51
|
+
end
|
52
|
+
|
53
|
+
def subscriber_url=(other)
|
54
|
+
@subscriber_url = Hootenanny::URI.parse(other)
|
55
|
+
end
|
56
|
+
|
57
|
+
def topic_url=(other)
|
58
|
+
@topic_url = Hootenanny::URI.parse(other)
|
59
|
+
end
|
60
|
+
|
61
|
+
def local_feed
|
62
|
+
local_feed_store.fetch(url: topic_url,
|
63
|
+
path: subscriber_url.to_digest)
|
64
|
+
end
|
65
|
+
|
66
|
+
def remote_feed
|
67
|
+
remote_feed_store.fetch(url: topic_url,
|
68
|
+
path: subscriber_url.to_digest)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|