lotus 0.0.12

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.
Files changed (48) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +16 -0
  4. data/README.md +233 -0
  5. data/Rakefile +7 -0
  6. data/lib/lotus.rb +232 -0
  7. data/lib/lotus/activity.rb +134 -0
  8. data/lib/lotus/atom/account.rb +50 -0
  9. data/lib/lotus/atom/address.rb +56 -0
  10. data/lib/lotus/atom/author.rb +167 -0
  11. data/lib/lotus/atom/category.rb +41 -0
  12. data/lib/lotus/atom/entry.rb +159 -0
  13. data/lib/lotus/atom/feed.rb +174 -0
  14. data/lib/lotus/atom/generator.rb +40 -0
  15. data/lib/lotus/atom/link.rb +79 -0
  16. data/lib/lotus/atom/name.rb +57 -0
  17. data/lib/lotus/atom/organization.rb +62 -0
  18. data/lib/lotus/atom/portable_contacts.rb +117 -0
  19. data/lib/lotus/atom/source.rb +168 -0
  20. data/lib/lotus/atom/thread.rb +60 -0
  21. data/lib/lotus/author.rb +177 -0
  22. data/lib/lotus/category.rb +45 -0
  23. data/lib/lotus/crypto.rb +146 -0
  24. data/lib/lotus/feed.rb +190 -0
  25. data/lib/lotus/generator.rb +53 -0
  26. data/lib/lotus/identity.rb +59 -0
  27. data/lib/lotus/link.rb +56 -0
  28. data/lib/lotus/notification.rb +220 -0
  29. data/lib/lotus/publisher.rb +40 -0
  30. data/lib/lotus/subscription.rb +117 -0
  31. data/lib/lotus/version.rb +3 -0
  32. data/lotus.gemspec +27 -0
  33. data/spec/activity_spec.rb +84 -0
  34. data/spec/atom/feed_spec.rb +681 -0
  35. data/spec/author_spec.rb +150 -0
  36. data/spec/crypto_spec.rb +138 -0
  37. data/spec/feed_spec.rb +252 -0
  38. data/spec/helper.rb +8 -0
  39. data/spec/identity_spec.rb +67 -0
  40. data/spec/link_spec.rb +30 -0
  41. data/spec/notification_spec.rb +77 -0
  42. data/test/example_feed.atom +393 -0
  43. data/test/example_feed_empty_author.atom +336 -0
  44. data/test/example_feed_false_connected.atom +359 -0
  45. data/test/example_feed_link_without_href.atom +134 -0
  46. data/test/example_page.html +4 -0
  47. data/test/mime_type_bug_feed.atom +874 -0
  48. metadata +204 -0
@@ -0,0 +1,177 @@
1
+ require 'lotus/activity'
2
+
3
+ module Lotus
4
+ require 'atom'
5
+
6
+ # Holds information about the author of the Feed.
7
+ class Author
8
+ require 'date'
9
+
10
+ # Holds the id that represents this contact.
11
+ attr_reader :id
12
+
13
+ # Holds the nickname of this contact.
14
+ attr_reader :nickname
15
+
16
+ # Holds a hash representing information about the name of this contact.
17
+ #
18
+ # contains one or more of the following:
19
+ # :formatted => The full name of the contact
20
+ # :family_name => The family name. "Last name" in Western contexts.
21
+ # :given_name => The given name. "First name" in Western contexts.
22
+ # :middle_name => The middle name.
23
+ # :honorific_prefix => "Title" in Western contexts. (e.g. "Mr." "Mrs.")
24
+ # :honorific_suffix => "Suffix" in Western contexts. (e.g. "Esq.")
25
+ attr_reader :extended_name
26
+
27
+ # The uri that uniquely identifies the author.
28
+ attr_reader :uri
29
+
30
+ # The email address of the author.
31
+ attr_reader :email
32
+
33
+ # The name of the author
34
+ attr_reader :name
35
+
36
+ # Holds a hash representing the address of the contact.
37
+ #
38
+ # contains one or more of the following:
39
+ # :formatted => A formatted representating of the address. May
40
+ # contain newlines.
41
+ # :street_address => The full street address. May contain newlines.
42
+ # :locality => The city or locality component.
43
+ # :region => The state or region component.
44
+ # :postal_code => The zipcode or postal code component.
45
+ # :country => The country name component.
46
+ attr_reader :address
47
+
48
+ # Holds a hash representing an organization for this contact.
49
+ #
50
+ # contains one or more of the following:
51
+ # :name => The name of the organization (e.g. company, school,
52
+ # etc) This field is required. Will be used for sorting.
53
+ # :department => The department within the organization.
54
+ # :title => The title or role within the organization.
55
+ # :type => The type of organization. Canonical values include
56
+ # "job" or "school"
57
+ # :start_date => A DateTime representing when the contact joined
58
+ # the organization.
59
+ # :end_date => A DateTime representing when the contact left the
60
+ # organization.
61
+ # :location => The physical location of this organization.
62
+ # :description => A free-text description of the role this contact
63
+ # played in this organization.
64
+ attr_reader :organization
65
+
66
+ # Holds a hash representing information about an account held by this
67
+ # contact.
68
+ #
69
+ # contains one or more of the following:
70
+ # :domain => The top-most authoriative domain for this account. (e.g.
71
+ # "twitter.com") This is the primary field. Is required.
72
+ # Used for sorting.
73
+ # :username => An alphanumeric username, typically chosen by the user.
74
+ # :userid => A user id, typically assigned, that uniquely refers to
75
+ # the user.
76
+ attr_reader :account
77
+
78
+ # Holds the gender of this contact.
79
+ attr_reader :gender
80
+
81
+ # Holds a note for this contact.
82
+ attr_reader :note
83
+
84
+ # Holds the display name for this contact.
85
+ attr_reader :display_name
86
+
87
+ # Holds the preferred username of this contact.
88
+ attr_reader :preferred_username
89
+
90
+ # Holds a DateTime that represents when this contact was last modified.
91
+ attr_reader :updated
92
+
93
+ # Holds a DateTime that represents when this contact was originally
94
+ # published.
95
+ attr_reader :published
96
+
97
+ # Holds a DateTime representing this contact's birthday.
98
+ attr_reader :birthday
99
+
100
+ # Holds a DateTime representing a contact's anniversary.
101
+ attr_reader :anniversary
102
+
103
+ # Creates a representating of an author.
104
+ #
105
+ # options:
106
+ # name => The name of the author. Defaults: "anonymous"
107
+ # id => The identifier that uniquely identifies the
108
+ # contact.
109
+ # nickname => The nickname of the contact.
110
+ # gender => The gender of the contact.
111
+ # note => A note for this contact.
112
+ # display_name => The display name for this contact.
113
+ # preferred_username => The preferred username for this contact.
114
+ # updated => A DateTime representing when this contact was
115
+ # last updated.
116
+ # published => A DateTime representing when this contact was
117
+ # originally created.
118
+ # birthday => A DateTime representing a birthday for this
119
+ # contact.
120
+ # anniversary => A DateTime representing an anniversary for this
121
+ # contact.
122
+ # extended_name => A Hash representing the name of the contact.
123
+ # organization => A Hash representing the organization of which the
124
+ # contact belongs.
125
+ # account => A Hash describing the authorative account for the
126
+ # author.
127
+ # address => A Hash describing the address of the contact.
128
+ # uri => The uri that uniquely identifies this author.
129
+ # email => The email of the author.
130
+ def initialize(options = {})
131
+ @uri = options[:uri]
132
+ @name = options[:name] || "anonymous"
133
+ @email = options[:email]
134
+
135
+ @id = options[:id]
136
+ @name = options[:name]
137
+ @gender = options[:gender]
138
+ @note = options[:note]
139
+ @nickname = options[:nickname]
140
+ @display_name = options[:display_name]
141
+ @preferred_username = options[:preferred_username]
142
+ @updated = options[:updated]
143
+ @published = options[:published]
144
+ @birthday = options[:birthday]
145
+ @anniversary = options[:anniversary]
146
+
147
+ @extended_name = options[:extended_name]
148
+ @organization = options[:organization]
149
+ @account = options[:account]
150
+ @address = options[:address]
151
+ end
152
+
153
+ def to_hash
154
+ {
155
+ :uri => self.uri,
156
+ :email => self.email,
157
+ :name => self.name,
158
+
159
+ :id => self.id,
160
+ :gender => self.gender,
161
+ :note => self.note,
162
+ :nickname => self.nickname,
163
+ :display_name => self.display_name,
164
+ :preferred_username => self.preferred_username,
165
+ :updated => self.updated,
166
+ :published => self.published,
167
+ :birthday => self.birthday,
168
+ :anniversary => self.anniversary,
169
+
170
+ :extended_name => self.extended_name,
171
+ :organization => self.organization,
172
+ :account => self.account,
173
+ :address => self.address
174
+ }
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,45 @@
1
+ module Lotus
2
+ # This element conveys information about a category associated with an entry
3
+ # or feed. There is no defined meaning to the content according to the Atom
4
+ # specification.
5
+ class Category
6
+ # Holds the base URI for relative URIs contained in scheme.
7
+ attr_reader :base
8
+
9
+ # Holds the language of term and label, when it exists. The language
10
+ # should be specified as RFC 3066 as either 2 or 3 letter codes.
11
+ # For example: 'en' for English or more specifically 'en-us'
12
+ attr_reader :lang
13
+
14
+ # Holds the optional scheme used for categorization.
15
+ attr_reader :scheme
16
+
17
+ # Holds the string identifying the category to which the entry or
18
+ # feed belongs.
19
+ attr_reader :term
20
+
21
+ # Holds the string that provides a human-readable label for display in
22
+ # end-user applications. The content of this field is language sensitive.
23
+ attr_reader :label
24
+
25
+ # Create a Category to apply to a feed or entry.
26
+ def initialize(options = {})
27
+ @base = options[:base]
28
+ @lang = options[:lang]
29
+ @scheme = options[:scheme]
30
+ @term = options[:term]
31
+ @label = options[:label]
32
+ end
33
+
34
+ # Yields a Hash that represents this category.
35
+ def to_hash
36
+ {
37
+ :base => @base,
38
+ :lang => @lang,
39
+ :scheme => @scheme,
40
+ :term => @term,
41
+ :label => @label
42
+ }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,146 @@
1
+ module Lotus
2
+ module Crypto
3
+ require 'openssl'
4
+ require 'rsa'
5
+ require 'base64'
6
+
7
+ KeyPair = Struct.new(:public_key, :private_key)
8
+
9
+ # Generate a new RSA keypair with the given bitlength.
10
+ def self.new_keypair(bits = 2048)
11
+ keypair = KeyPair.new
12
+
13
+ key = RSA::KeyPair.generate(bits)
14
+
15
+ public_key = key.public_key
16
+ m = public_key.modulus
17
+ e = public_key.exponent
18
+
19
+ modulus = ""
20
+ until m == 0 do
21
+ modulus << [m % 256].pack("C")
22
+ m >>= 8
23
+ end
24
+ modulus.reverse!
25
+
26
+ exponent = ""
27
+ until e == 0 do
28
+ exponent << [e % 256].pack("C")
29
+ e >>= 8
30
+ end
31
+ exponent.reverse!
32
+
33
+ keypair.public_key = "RSA.#{Base64::urlsafe_encode64(modulus)}.#{Base64::urlsafe_encode64(exponent)}"
34
+
35
+ tmp_private_key = key.private_key
36
+ m = tmp_private_key.modulus
37
+ e = tmp_private_key.exponent
38
+
39
+ modulus = ""
40
+ until m == 0 do
41
+ modulus << [m % 256].pack("C")
42
+ m >>= 8
43
+ end
44
+ modulus.reverse!
45
+
46
+ exponent = ""
47
+ until e == 0 do
48
+ exponent << [e % 256].pack("C")
49
+ e >>= 8
50
+ end
51
+ exponent.reverse!
52
+
53
+ keypair.private_key = "RSA.#{Base64::urlsafe_encode64(modulus)}.#{Base64::urlsafe_encode64(exponent)}"
54
+
55
+ keypair
56
+ end
57
+
58
+ # Creates an EMSA signature for the given plaintext and key.
59
+ def self.emsa_sign(text, private_key)
60
+ private_key = generate_key(private_key) unless private_key.is_a? RSA::Key
61
+
62
+ signature = self.emsa_signature(text, private_key)
63
+
64
+ self.decrypt(private_key, signature)
65
+ end
66
+
67
+ # Verifies an existing EMSA signature.
68
+ def self.emsa_verify(text, signature, public_key)
69
+ # RSA encryption is needed to compare the signatures
70
+ public_key = generate_key(public_key) unless public_key.is_a? RSA::Key
71
+
72
+ # Get signature to check
73
+ emsa = self.emsa_signature(text, public_key)
74
+
75
+ # Get signature in payload
76
+ emsa_signature = self.encrypt(public_key, signature)
77
+
78
+ # RSA gem drops leading 0s since it does math upon an Integer
79
+ # As a workaround, I check for what I expect the second byte to be (\x01)
80
+ # This workaround will also handle seeing a \x00 first if the RSA gem is
81
+ # fixed.
82
+ if emsa_signature.getbyte(0) == 1
83
+ emsa_signature = "\x00#{emsa_signature}"
84
+ end
85
+
86
+ # Does the signature match?
87
+ # Return the result.
88
+ emsa_signature == emsa
89
+ end
90
+
91
+ # Decrypts the given data with the given private key.
92
+ def self.decrypt(private_key, data)
93
+ private_key = generate_key(private_key) unless private_key.is_a? RSA::Key
94
+ keypair = generate_keypair(nil, private_key)
95
+ keypair.decrypt(data)
96
+ end
97
+
98
+ # Encrypts the given data with the given public key.
99
+ def self.encrypt(public_key, data)
100
+ public_key = generate_key(public_key) unless public_key.is_a? RSA::Key
101
+ keypair = generate_keypair(public_key, nil)
102
+ keypair.encrypt(data)
103
+ end
104
+
105
+ private
106
+
107
+ # :nodoc:
108
+ def self.emsa_signature(text, key)
109
+ modulus_byte_count = key.modulus.size
110
+
111
+ plaintext = Digest::SHA2.new(256).digest(text)
112
+
113
+ prefix = "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20"
114
+ padding_count = modulus_byte_count - prefix.bytes.count - plaintext.bytes.count - 3
115
+
116
+ padding = ""
117
+ padding_count.times do
118
+ padding = padding + "\xff"
119
+ end
120
+
121
+ "\x00\x01#{padding}\x00#{prefix}#{plaintext}".force_encoding('binary')
122
+ end
123
+
124
+ # :nodoc:
125
+ def self.generate_key(key_string)
126
+ return nil unless key_string
127
+
128
+ key_string.match /^RSA\.(.*?)\.(.*)$/
129
+
130
+ modulus = decode_key($1)
131
+ exponent = decode_key($2)
132
+
133
+ RSA::Key.new(modulus, exponent)
134
+ end
135
+
136
+ def self.generate_keypair(public_key, private_key)
137
+ RSA::KeyPair.new(private_key, public_key)
138
+ end
139
+
140
+ # :nodoc:
141
+ def self.decode_key(encoded_key_part)
142
+ modulus = Base64::urlsafe_decode64(encoded_key_part)
143
+ modulus.bytes.inject(0) {|num, byte| (num << 8) | byte }
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,190 @@
1
+ require 'lotus/activity'
2
+ require 'lotus/author'
3
+
4
+ module Lotus
5
+ require 'atom'
6
+
7
+ # This class represents a Lotus::Feed object.
8
+ class Feed
9
+ require 'open-uri'
10
+ require 'date'
11
+
12
+ # Holds the id that uniquely represents this feed.
13
+ attr_reader :id
14
+
15
+ # Holds the url that represents this feed.
16
+ attr_reader :url
17
+
18
+ # Holds the list of categories for this feed as Lotus::Category.
19
+ attr_reader :categories
20
+
21
+ # Holds human-readable information about the content rights of the entries
22
+ # in the feed without an explicit rights field of their own. SHOULD NOT be
23
+ # machine interpreted.
24
+ attr_reader :rights
25
+
26
+ # Holds the title for this feed.
27
+ attr_reader :title
28
+
29
+ # Holds the content-type for the title.
30
+ attr_reader :title_type
31
+
32
+ # Holds the subtitle for this feed.
33
+ attr_reader :subtitle
34
+
35
+ # Holds the content-type for the subtitle.
36
+ attr_reader :subtitle_type
37
+
38
+ # Holds the URL for the icon representing this feed.
39
+ attr_reader :icon
40
+
41
+ # Holds the URL for the logo representing this feed.
42
+ attr_reader :logo
43
+
44
+ # Holds the generator for this content as an Lotus::Generator.
45
+ attr_reader :generator
46
+
47
+ # Holds the list of contributors, if any, that are involved in this feed
48
+ # as Lotus::Author.
49
+ attr_reader :contributors
50
+
51
+ # Holds the DateTime when this feed was originally created.
52
+ attr_reader :published
53
+
54
+ # Holds the DateTime when this feed was last modified.
55
+ attr_reader :updated
56
+
57
+ # Holds the list of authors as Lotus::Author responsible for this feed.
58
+ attr_reader :authors
59
+
60
+ # Holds the list of entries as Lotus::Activity contained within this feed.
61
+ attr_reader :entries
62
+
63
+ # Holds the list of hubs that are available to manage subscriptions to this
64
+ # feed.
65
+ attr_reader :hubs
66
+
67
+ # Holds the salmon url that handles notifications for this feed.
68
+ attr_reader :salmon_url
69
+
70
+ # Holds links to other resources as an array of Lotus::Link
71
+ attr_reader :links
72
+
73
+ # Creates a new representation of a feed.
74
+ #
75
+ # options:
76
+ # id => The unique identifier for this feed.
77
+ # url => The url that represents this feed.
78
+ # title => The title for this feed. Defaults: "Untitled"
79
+ # title_type => The content type for the title.
80
+ # subtitle => The subtitle for this feed.
81
+ # subtitle_type => The content type for the subtitle.
82
+ # authors => The list of Lotus::Author's for this feed.
83
+ # Defaults: []
84
+ # contributors => The list of Lotus::Author's that contributed to this
85
+ # feed. Defaults: []
86
+ # entries => The list of Lotus::Activity's for this feed.
87
+ # Defaults: []
88
+ # icon => The url of the icon that represents this feed. It
89
+ # should have an aspect ratio of 1 horizontal to 1
90
+ # vertical and optimized for presentation at a
91
+ # small size.
92
+ # logo => The url of the logo that represents this feed. It
93
+ # should have an aspect ratio of 2 horizontal to 1
94
+ # vertical.
95
+ # categories => An array of Lotus::Category's that describe how to
96
+ # categorize and describe the content of the feed.
97
+ # Defaults: []
98
+ # rights => A String depicting the rights of entries without
99
+ # explicit rights of their own. SHOULD NOT be machine
100
+ # interpreted.
101
+ # updated => The DateTime representing when this feed was last
102
+ # modified.
103
+ # published => The DateTime representing when this feed was originally
104
+ # published.
105
+ # salmon_url => The url of the salmon endpoint, if one exists, for this
106
+ # feed.
107
+ # links => An array of Lotus::Link that adds relations to other
108
+ # resources.
109
+ # generator => A Lotus::Generator representing the agent
110
+ # responsible for generating this feed.
111
+ #
112
+ # Usage:
113
+ #
114
+ # author = Lotus::Author.new(:name => "Kelly")
115
+ #
116
+ # feed = Lotus::Feed.new(:title => "My Feed",
117
+ # :id => "1",
118
+ # :url => "http://example.com/feeds/1",
119
+ # :authors => [author])
120
+ def initialize(options = {})
121
+ @id = options[:id]
122
+ @url = options[:url]
123
+ @icon = options[:icon]
124
+ @logo = options[:logo]
125
+ @rights = options[:rights]
126
+ @title = options[:title] || "Untitled"
127
+ @title_type = options[:title_type]
128
+ @subtitle = options[:subtitle]
129
+ @subtitle_type = options[:subtitle_type]
130
+ @authors = options[:authors] || []
131
+ @categories = options[:categories] || []
132
+ @contributors = options[:contributors] || []
133
+ @entries = options[:entries] || []
134
+ @updated = options[:updated]
135
+ @published = options[:published]
136
+ @salmon_url = options[:salmon_url]
137
+ @hubs = options[:hubs] || []
138
+ @generator = options[:generator]
139
+ end
140
+
141
+ # Yields a Lotus::Link to this feed.
142
+ #
143
+ # options: Can override Lotus::Link properties, such as rel.
144
+ #
145
+ # Usage:
146
+ #
147
+ # feed = Lotus::Feed.new(:title => "Foo", :url => "http://example.com")
148
+ # feed.to_link(:rel => "alternate", :title => "Foo's Feed")
149
+ #
150
+ # Generates a link with:
151
+ # <Lotus::Link rel="alternate" title="Foo's Feed" url="http://example.com">
152
+ def to_link(options = {})
153
+ options = { :title => self.title,
154
+ :href => self.url }.merge(options)
155
+
156
+ Lotus::Link.new(options)
157
+ end
158
+
159
+ # Returns a hash of the properties of the feed.
160
+ def to_hash
161
+ {
162
+ :id => self.id,
163
+ :url => self.url,
164
+ :hubs => self.hubs.dup,
165
+ :icon => self.icon,
166
+ :logo => self.logo,
167
+ :rights => self.rights,
168
+ :title => self.title,
169
+ :title_type => self.title_type,
170
+ :subtitle => self.subtitle,
171
+ :subtitle_type => self.subtitle_type,
172
+ :authors => self.authors.dup,
173
+ :categories => self.categories.dup,
174
+ :contributors => self.contributors.dup,
175
+ :entries => self.entries.dup,
176
+ :updated => self.updated,
177
+ :salmon_url => self.salmon_url,
178
+ :published => self.published,
179
+ :generator => self.generator
180
+ }
181
+ end
182
+
183
+ # Returns a string containing an Atom representation of the feed.
184
+ def to_atom
185
+ require 'lotus/atom/feed'
186
+
187
+ Lotus::Atom::Feed.from_canonical(self).to_xml
188
+ end
189
+ end
190
+ end