lotus 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
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