hootenanny 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +5 -0
  3. data/Gemfile +5 -7
  4. data/Gemfile.lock +46 -28
  5. data/app/controllers/hootenanny/notifications_controller.rb +31 -0
  6. data/app/controllers/hootenanny/parameters.rb +16 -0
  7. data/app/controllers/hootenanny/subscriptions_controller.rb +24 -3
  8. data/app/models/hootenanny/publish_notification.rb +90 -0
  9. data/app/models/hootenanny/subscription.rb +72 -23
  10. data/config/routes.rb +2 -1
  11. data/db/migrate/20130607182642_add_started_at_and_lease_duration_to_subscriptions.rb +15 -0
  12. data/db/migrate/20130608225621_add_hmac_secret_to_subscription.rb +5 -0
  13. data/db/migrate/20130611235218_add_publish_notifications.rb +9 -0
  14. data/db/migrate/20130612153138_add_timestamps_to_publish_notification.rb +5 -0
  15. data/db/migrate/20130705200729_add_processed_flag_to_publish_notifications.rb +6 -0
  16. data/db/migrate/20130711061329_switch_publish_notification_process_state_from_boolean_to_string.rb +11 -0
  17. data/db/migrate/20130711061558_add_index_to_notifications_state.rb +5 -0
  18. data/hootenanny.gemspec +6 -2
  19. data/lib/hootenanny/configuration.rb +43 -0
  20. data/lib/hootenanny/correspondent.rb +44 -0
  21. data/lib/hootenanny/errors.rb +42 -1
  22. data/lib/hootenanny/feed.rb +75 -0
  23. data/lib/hootenanny/feed/atom_feed.rb +18 -0
  24. data/lib/hootenanny/feed/atom_feed_item.rb +8 -0
  25. data/lib/hootenanny/feed/digest_feed.rb +14 -0
  26. data/lib/hootenanny/feed/digest_feed_item.rb +11 -0
  27. data/lib/hootenanny/feed/feed_item.rb +30 -0
  28. data/lib/hootenanny/feed/file.rb +66 -0
  29. data/lib/hootenanny/feed/json_feed.rb +48 -0
  30. data/lib/hootenanny/feed/json_feed_item.rb +8 -0
  31. data/lib/hootenanny/feed/null_feed.rb +27 -0
  32. data/lib/hootenanny/feed/rss_feed.rb +52 -0
  33. data/lib/hootenanny/feed/rss_feed_item.rb +8 -0
  34. data/lib/hootenanny/feed_store.rb +30 -0
  35. data/lib/hootenanny/feed_store/file_feed_store.rb +55 -0
  36. data/lib/hootenanny/feed_store/web_feed_store.rb +42 -0
  37. data/lib/hootenanny/hub.rb +116 -22
  38. data/lib/hootenanny/publish_notification_expiration_policy.rb +17 -0
  39. data/lib/hootenanny/request.rb +40 -0
  40. data/lib/hootenanny/request/publish_notification.rb +94 -0
  41. data/lib/hootenanny/request/subscription.rb +153 -0
  42. data/lib/hootenanny/subscription_delivery.rb +47 -0
  43. data/lib/hootenanny/topic.rb +38 -0
  44. data/lib/hootenanny/topic_synchronizer.rb +71 -0
  45. data/lib/hootenanny/uri.rb +53 -0
  46. data/lib/hootenanny/verification.rb +108 -0
  47. data/lib/hootenanny/version.rb +1 -1
  48. data/spec/dummy/config/database.yml +12 -6
  49. data/spec/dummy/db/migrate/20130607183149_add_started_at_and_lease_duration_to_subscriptions.hootenanny.rb +16 -0
  50. data/spec/dummy/db/migrate/20130608231253_add_hmac_secret_to_subscription.hootenanny.rb +6 -0
  51. data/spec/dummy/db/migrate/20130611235546_add_publish_notifications.hootenanny.rb +10 -0
  52. data/spec/dummy/db/migrate/20130612153353_add_timestamps_to_publish_notification.hootenanny.rb +6 -0
  53. data/spec/dummy/db/migrate/20130705200832_add_processed_flag_to_publish_notifications.hootenanny.rb +7 -0
  54. data/spec/dummy/db/migrate/20130711061518_switch_publish_notification_process_state_from_boolean_to_string.hootenanny.rb +12 -0
  55. data/spec/dummy/db/migrate/20130711061629_add_index_to_notifications_state.hootenanny.rb +6 -0
  56. data/spec/factories/publish_notification.rb +13 -0
  57. data/spec/factories/requests/publish_notification.rb +11 -0
  58. data/spec/factories/requests/subscription.rb +46 -0
  59. data/spec/factories/subscription.rb +14 -2
  60. data/spec/factories/verification.rb +15 -0
  61. data/spec/features/publishers/can_notify_the_hub_of_content_updates_spec.rb +58 -0
  62. data/spec/features/subscribers/are_protected_from_unwarranted_subscriptions_spec.rb +59 -0
  63. data/spec/features/subscribers/can_receive_distributions_of_topic_content_spec.rb +175 -0
  64. data/spec/features/subscribers/can_subscribe_to_a_topic_spec.rb +76 -7
  65. data/spec/fixtures/feeds/atom/97d79220f68b4bf27.atom +0 -0
  66. data/spec/fixtures/feeds/atom/sample_feed.atom +51 -0
  67. data/spec/fixtures/feeds/digest/97d79220f68b4bf27.digest +1 -0
  68. data/spec/fixtures/feeds/digest/complete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +6 -0
  69. data/spec/fixtures/feeds/digest/incomplete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +5 -0
  70. data/spec/fixtures/feeds/digest/sample_feed.digest +6 -0
  71. data/spec/fixtures/feeds/json/97d79220f68b4bf27.json +1 -0
  72. data/spec/fixtures/feeds/json/feed_with_one_item.json +21 -0
  73. data/spec/fixtures/feeds/json/sample_feed.json +30 -0
  74. data/spec/fixtures/feeds/rss/5b187098da59f077f/97d79220f68b4bf27.rss +21 -0
  75. data/spec/fixtures/feeds/rss/97d79220f68b4bf27.rss +0 -0
  76. data/spec/fixtures/feeds/rss/feed_with_one_item.rss +15 -0
  77. data/spec/fixtures/feeds/rss/minimal_feed.rss +12 -0
  78. data/spec/fixtures/feeds/rss/sample_feed.rss +22 -0
  79. data/spec/fixtures/feeds/rss/sample_feed_2.rss +22 -0
  80. data/spec/lib/hootenanny/configuration_spec.rb +7 -0
  81. data/spec/lib/hootenanny/correspondent_spec.rb +94 -0
  82. data/spec/lib/hootenanny/errors_spec.rb +21 -0
  83. data/spec/lib/hootenanny/feed/atom_feed_item_spec.rb +9 -0
  84. data/spec/lib/hootenanny/feed/atom_feed_spec.rb +40 -0
  85. data/spec/lib/hootenanny/feed/digest_feed_item_spec.rb +9 -0
  86. data/spec/lib/hootenanny/feed/digest_feed_spec.rb +40 -0
  87. data/spec/lib/hootenanny/feed/feed_item_spec.rb +49 -0
  88. data/spec/lib/hootenanny/feed/file_spec.rb +66 -0
  89. data/spec/lib/hootenanny/feed/json_feed_item_spec.rb +9 -0
  90. data/spec/lib/hootenanny/feed/json_feed_spec.rb +128 -0
  91. data/spec/lib/hootenanny/feed/null_feed_spec.rb +9 -0
  92. data/spec/lib/hootenanny/feed/rss_feed_item_spec.rb +9 -0
  93. data/spec/lib/hootenanny/feed/rss_feed_spec.rb +143 -0
  94. data/spec/lib/hootenanny/feed_spec.rb +159 -0
  95. data/spec/lib/hootenanny/feed_store/file_feed_store_spec.rb +58 -0
  96. data/spec/lib/hootenanny/feed_store/web_feed_store_spec.rb +47 -0
  97. data/spec/lib/hootenanny/feed_store_spec.rb +27 -0
  98. data/spec/lib/hootenanny/hub_spec.rb +73 -0
  99. data/spec/lib/hootenanny/publish_notification_expiration_policy_spec.rb +35 -0
  100. data/spec/lib/hootenanny/request/publish_notification_spec.rb +43 -0
  101. data/spec/lib/hootenanny/request/subscription_spec.rb +89 -0
  102. data/spec/lib/hootenanny/request_spec.rb +21 -0
  103. data/spec/lib/hootenanny/subscription_delivery_spec.rb +54 -0
  104. data/spec/lib/hootenanny/topic_spec.rb +15 -0
  105. data/spec/lib/hootenanny/topic_synchronizer_spec.rb +98 -0
  106. data/spec/lib/hootenanny/uri_spec.rb +32 -0
  107. data/spec/lib/hootenanny/verification_spec.rb +92 -0
  108. data/spec/models/hootenanny/publish_notification_spec.rb +55 -0
  109. data/spec/models/hootenanny/subscription_spec.rb +58 -19
  110. data/spec/support/verification.rb +63 -0
  111. metadata +231 -14
  112. 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